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

Merge pull request #927 from QuentinHsu/refactor-system-setting

# Conflicts:
#	web/src/App.js
#	web/src/components/ModelSetting.js
#	web/src/components/PersonalSetting.js
#	web/src/components/SystemSetting.js
#	web/src/pages/Channel/EditChannel.js
Apple\Apple 8 месяцев назад
Родитель
Сommit
71d0d759da
74 измененных файлов с 5681 добавлено и 3641 удалено
  1. 1 1
      web/.prettierrc.mjs
  2. 866 277
      web/pnpm-lock.yaml
  3. 23 23
      web/src/App.js
  4. 260 183
      web/src/components/ChannelsTable.js
  5. 7 9
      web/src/components/Footer.js
  6. 160 101
      web/src/components/HeaderBar.js
  7. 21 12
      web/src/components/LoginForm.js
  8. 301 215
      web/src/components/LogsTable.js
  9. 13 15
      web/src/components/MjLogsTable.js
  10. 73 52
      web/src/components/ModelPricing.js
  11. 3 6
      web/src/components/ModelSetting.js
  12. 47 45
      web/src/components/OAuth2Callback.js
  13. 31 15
      web/src/components/OIDCIcon.js
  14. 5 6
      web/src/components/OperationSetting.js
  15. 162 115
      web/src/components/OtherSetting.js
  16. 71 53
      web/src/components/PageLayout.js
  17. 192 104
      web/src/components/PersonalSetting.js
  18. 1 4
      web/src/components/RateLimitSetting.js
  19. 50 42
      web/src/components/RedemptionsTable.js
  20. 59 34
      web/src/components/RegisterForm.js
  21. 76 38
      web/src/components/SiderBar.js
  22. 707 545
      web/src/components/SystemSetting.js
  23. 480 368
      web/src/components/TaskLogsTable.js
  24. 47 41
      web/src/components/TokensTable.js
  25. 31 19
      web/src/components/UsersTable.js
  26. 11 4
      web/src/components/custom/TextInput.js
  27. 3 3
      web/src/components/custom/TextNumberInput.js
  28. 3 3
      web/src/components/fetchTokenKeys.js
  29. 3 4
      web/src/components/utils.js
  30. 20 20
      web/src/constants/channel.constants.js
  31. 18 12
      web/src/context/Style/index.js
  32. 4 4
      web/src/helpers/api.js
  33. 7 7
      web/src/helpers/other.js
  34. 342 225
      web/src/helpers/render.js
  35. 3 4
      web/src/helpers/utils.js
  36. 6 6
      web/src/i18n/i18n.js
  37. 1 1
      web/src/i18n/locales/en.json
  38. 1 1
      web/src/i18n/locales/zh.json
  39. 57 12
      web/src/index.css
  40. 1 1
      web/src/index.js
  41. 181 145
      web/src/pages/Channel/EditChannel.js
  42. 64 54
      web/src/pages/Channel/EditTagModal.js
  43. 4 4
      web/src/pages/Channel/index.js
  44. 22 22
      web/src/pages/Chat/index.js
  45. 2 2
      web/src/pages/Chat2Link/index.js
  46. 92 65
      web/src/pages/Detail/index.js
  47. 20 16
      web/src/pages/Home/index.js
  48. 149 106
      web/src/pages/Playground/Playground.js
  49. 8 2
      web/src/pages/Redemption/EditRedemption.js
  50. 9 9
      web/src/pages/Redemption/index.js
  51. 60 22
      web/src/pages/Setting/Model/SettingClaudeModel.js
  52. 30 15
      web/src/pages/Setting/Model/SettingGeminiModel.js
  53. 13 4
      web/src/pages/Setting/Model/SettingGlobalModel.js
  54. 56 43
      web/src/pages/Setting/Operation/GroupRatioSettings.js
  55. 77 50
      web/src/pages/Setting/Operation/ModelRatioSettings.js
  56. 152 93
      web/src/pages/Setting/Operation/ModelRationNotSetEditor.js
  57. 294 198
      web/src/pages/Setting/Operation/ModelSettingsVisualEditor.js
  58. 62 47
      web/src/pages/Setting/Operation/SettingsChats.js
  59. 2 1
      web/src/pages/Setting/Operation/SettingsCreditLimit.js
  60. 3 2
      web/src/pages/Setting/Operation/SettingsDataDashboard.js
  61. 4 2
      web/src/pages/Setting/Operation/SettingsDrawing.js
  62. 16 4
      web/src/pages/Setting/Operation/SettingsGeneral.js
  63. 2 1
      web/src/pages/Setting/Operation/SettingsLog.js
  64. 14 6
      web/src/pages/Setting/Operation/SettingsMonitoring.js
  65. 2 1
      web/src/pages/Setting/Operation/SettingsSensitiveWords.js
  66. 3 2
      web/src/pages/Setting/RateLimit/SettingsRequestRateLimit.js
  67. 105 57
      web/src/pages/Setup/index.js
  68. 1 1
      web/src/pages/Task/index.js
  69. 19 13
      web/src/pages/Token/EditToken.js
  70. 9 7
      web/src/pages/Token/index.js
  71. 9 3
      web/src/pages/TopUp/index.js
  72. 21 9
      web/src/pages/User/EditUser.js
  73. 4 4
      web/src/pages/User/index.js
  74. 5 1
      web/vite.config.js

+ 1 - 1
web/.prettierrc.mjs

@@ -1 +1 @@
-module.exports = require("@so1ve/prettier-config");
+module.exports = require('@so1ve/prettier-config');

Разница между файлами не показана из-за своего большого размера
+ 866 - 277
web/pnpm-lock.yaml


+ 23 - 23
web/src/App.js

@@ -21,9 +21,9 @@ import Chat2Link from './pages/Chat2Link';
 import { Layout } from '@douyinfe/semi-ui';
 import Midjourney from './pages/Midjourney';
 import Pricing from './pages/Pricing/index.js';
-import Task from "./pages/Task/index.js";
+import Task from './pages/Task/index.js';
 import Playground from './pages/Playground/Playground.js';
-import OAuth2Callback from "./components/OAuth2Callback.js";
+import OAuth2Callback from './components/OAuth2Callback.js';
 import PersonalSetting from './components/PersonalSetting.js';
 import Setup from './pages/Setup/index.js';
 import SetupCheck from './components/SetupCheck';
@@ -34,7 +34,7 @@ const About = lazy(() => import('./pages/About'));
 
 function App() {
   const location = useLocation();
-  
+
   return (
     <SetupCheck>
       <Routes>
@@ -167,18 +167,18 @@ function App() {
           }
         />
         <Route
-            path='/oauth/oidc'
-            element={
-                <Suspense fallback={<Loading></Loading>}>
-                    <OAuth2Callback type='oidc'></OAuth2Callback>
-                </Suspense>
-            }
+          path='/oauth/oidc'
+          element={
+            <Suspense fallback={<Loading></Loading>}>
+              <OAuth2Callback type='oidc'></OAuth2Callback>
+            </Suspense>
+          }
         />
         <Route
           path='/oauth/linuxdo'
           element={
             <Suspense fallback={<Loading></Loading>} key={location.pathname}>
-                <OAuth2Callback type='linuxdo'></OAuth2Callback>
+              <OAuth2Callback type='linuxdo'></OAuth2Callback>
             </Suspense>
           }
         />
@@ -275,19 +275,19 @@ function App() {
           }
         />
         {/* 方便使用chat2link直接跳转聊天... */}
-          <Route
-            path='/chat2link'
-            element={
-              <PrivateRoute>
-                <Suspense fallback={<Loading></Loading>} key={location.pathname}>
-                    <Chat2Link />
-                </Suspense>
-              </PrivateRoute>
-            }
-          />
-          <Route path='*' element={<NotFound />} />
-        </Routes>
-      </SetupCheck>
+        <Route
+          path='/chat2link'
+          element={
+            <PrivateRoute>
+              <Suspense fallback={<Loading></Loading>} key={location.pathname}>
+                <Chat2Link />
+              </Suspense>
+            </PrivateRoute>
+          }
+        />
+        <Route path='*' element={<NotFound />} />
+      </Routes>
+    </SetupCheck>
   );
 }
 

Разница между файлами не показана из-за своего большого размера
+ 260 - 183
web/src/components/ChannelsTable.js


+ 7 - 9
web/src/components/Footer.js

@@ -28,11 +28,7 @@ const FooterBar = () => {
         New API {import.meta.env.VITE_REACT_APP_VERSION}{' '}
       </a>
       {t('由')}{' '}
-      <a
-        href='https://github.com/Calcium-Ion'
-        target='_blank'
-        rel='noreferrer'
-      >
+      <a href='https://github.com/Calcium-Ion' target='_blank' rel='noreferrer'>
         Calcium-Ion
       </a>{' '}
       {t('开发,基于')}{' '}
@@ -59,10 +55,12 @@ const FooterBar = () => {
   }, []);
 
   return (
-    <div style={{
-      textAlign: 'center',
-      paddingBottom: '5px',
-    }}>
+    <div
+      style={{
+        textAlign: 'center',
+        paddingBottom: '5px',
+      }}
+    >
       {footer ? (
         <div
           className='custom-footer'

+ 160 - 101
web/src/components/HeaderBar.js

@@ -13,18 +13,28 @@ import {
   IconClose,
   IconHelpCircle,
   IconHome,
-  IconHomeStroked, IconIndentLeft,
+  IconHomeStroked,
+  IconIndentLeft,
   IconComment,
-  IconKey, IconMenu,
+  IconKey,
+  IconMenu,
   IconNoteMoneyStroked,
   IconPriceTag,
   IconUser,
   IconLanguage,
   IconInfoCircle,
   IconCreditCard,
-  IconTerminal
+  IconTerminal,
 } from '@douyinfe/semi-icons';
-import { Avatar, Button, Dropdown, Layout, Nav, Switch, Tag } from '@douyinfe/semi-ui';
+import {
+  Avatar,
+  Button,
+  Dropdown,
+  Layout,
+  Nav,
+  Switch,
+  Tag,
+} from '@douyinfe/semi-ui';
 import { stringToColor } from '../helpers/render';
 import Text from '@douyinfe/semi-ui/lib/es/typography/text';
 import { StyleContext } from '../context/Style/index.js';
@@ -36,20 +46,20 @@ const headerStyle = {
   borderBottom: '1px solid var(--semi-color-border)',
   background: 'var(--semi-color-bg-0)',
   transition: 'all 0.3s ease',
-  width: '100%'
+  width: '100%',
 };
 
 // 自定义顶部栏按钮样式
 const headerItemStyle = {
   borderRadius: '4px',
   margin: '0 4px',
-  transition: 'all 0.3s ease'
+  transition: 'all 0.3s ease',
 };
 
 // 自定义顶部栏按钮悬停样式
 const headerItemHoverStyle = {
   backgroundColor: 'var(--semi-color-primary-light-default)',
-  color: 'var(--semi-color-primary)'
+  color: 'var(--semi-color-primary)',
 };
 
 // 自定义顶部栏Logo样式
@@ -58,23 +68,24 @@ const logoStyle = {
   alignItems: 'center',
   gap: '10px',
   padding: '0 10px',
-  height: '100%'
+  height: '100%',
 };
 
 // 自定义顶部栏系统名称样式
 const systemNameStyle = {
   fontWeight: 'bold',
   fontSize: '18px',
-  background: 'linear-gradient(45deg, var(--semi-color-primary), var(--semi-color-secondary))',
+  background:
+    'linear-gradient(45deg, var(--semi-color-primary), var(--semi-color-secondary))',
   WebkitBackgroundClip: 'text',
   WebkitTextFillColor: 'transparent',
-  padding: '0 5px'
+  padding: '0 5px',
 };
 
 // 自定义顶部栏按钮图标样式
 const headerIconStyle = {
   fontSize: '18px',
-  transition: 'all 0.3s ease'
+  transition: 'all 0.3s ease',
 };
 
 // 自定义头像样式
@@ -82,19 +93,19 @@ const avatarStyle = {
   margin: '4px',
   cursor: 'pointer',
   boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
-  transition: 'all 0.3s ease'
+  transition: 'all 0.3s ease',
 };
 
 // 自定义下拉菜单样式
 const dropdownStyle = {
   borderRadius: '8px',
   boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
-  overflow: 'hidden'
+  overflow: 'hidden',
 };
 
 // 自定义主题切换开关样式
 const switchStyle = {
-  margin: '0 8px'
+  margin: '0 8px',
 };
 
 const HeaderBar = () => {
@@ -109,8 +120,7 @@ const HeaderBar = () => {
   const logo = getLogo();
   const currentDate = new Date();
   // enable fireworks on new year(1.1 and 2.9-2.24)
-  const isNewYear =
-    (currentDate.getMonth() === 0 && currentDate.getDate() === 1);
+  const isNewYear = currentDate.getMonth() === 0 && currentDate.getDate() === 1;
 
   // Check if self-use mode is enabled
   const isSelfUseMode = statusState?.status?.self_use_mode_enabled || false;
@@ -137,13 +147,17 @@ const HeaderBar = () => {
       icon: <IconPriceTag style={headerIconStyle} />,
     },
     // Only include the docs button if docsLink exists
-    ...(docsLink ? [{
-      text: t('文档'),
-      itemKey: 'docs',
-      isExternal: true,
-      externalLink: docsLink,
-      icon: <IconHelpCircle style={headerIconStyle} />,
-    }] : []),
+    ...(docsLink
+      ? [
+          {
+            text: t('文档'),
+            itemKey: 'docs',
+            isExternal: true,
+            externalLink: docsLink,
+            icon: <IconHelpCircle style={headerIconStyle} />,
+          },
+        ]
+      : []),
     {
       text: t('关于'),
       itemKey: 'about',
@@ -232,30 +246,38 @@ const HeaderBar = () => {
                 chat: '/chat',
               };
               return (
-                <div onClick={(e) => {
-                  if (props.itemKey === 'home') {
-                    styleDispatch({ type: 'SET_INNER_PADDING', payload: false });
-                    styleDispatch({ type: 'SET_SIDER', payload: false });
-                  } else {
-                    styleDispatch({ type: 'SET_INNER_PADDING', payload: true });
-                    if (!styleState.isMobile) {
-                      styleDispatch({ type: 'SET_SIDER', payload: true });
+                <div
+                  onClick={(e) => {
+                    if (props.itemKey === 'home') {
+                      styleDispatch({
+                        type: 'SET_INNER_PADDING',
+                        payload: false,
+                      });
+                      styleDispatch({ type: 'SET_SIDER', payload: false });
+                    } else {
+                      styleDispatch({
+                        type: 'SET_INNER_PADDING',
+                        payload: true,
+                      });
+                      if (!styleState.isMobile) {
+                        styleDispatch({ type: 'SET_SIDER', payload: true });
+                      }
                     }
-                  }
-                }}>
+                  }}
+                >
                   {props.isExternal ? (
                     <a
-                      className="header-bar-text"
+                      className='header-bar-text'
                       style={{ textDecoration: 'none' }}
                       href={props.externalLink}
-                      target="_blank"
-                      rel="noopener noreferrer"
+                      target='_blank'
+                      rel='noopener noreferrer'
                     >
                       {itemElement}
                     </a>
                   ) : (
                     <Link
-                      className="header-bar-text"
+                      className='header-bar-text'
                       style={{ textDecoration: 'none' }}
                       to={routerMap[props.itemKey]}
                     >
@@ -268,67 +290,98 @@ const HeaderBar = () => {
             selectedKeys={[]}
             // items={headerButtons}
             onSelect={(key) => {}}
-            header={styleState.isMobile?{
-              logo: (
-                <div style={{ display: 'flex', alignItems: 'center', position: 'relative' }}>
-                  {
-                    !styleState.showSider ?
-                      <Button icon={<IconMenu />} theme="light" aria-label={t('展开侧边栏')} onClick={
-                        () => styleDispatch({ type: 'SET_SIDER', payload: true })
-                      } />:
-                      <Button icon={<IconIndentLeft />} theme="light" aria-label={t('闭侧边栏')} onClick={
-                        () => styleDispatch({ type: 'SET_SIDER', payload: false })
-                      } />
+            header={
+              styleState.isMobile
+                ? {
+                    logo: (
+                      <div
+                        style={{
+                          display: 'flex',
+                          alignItems: 'center',
+                          position: 'relative',
+                        }}
+                      >
+                        {!styleState.showSider ? (
+                          <Button
+                            icon={<IconMenu />}
+                            theme='light'
+                            aria-label={t('展开侧边栏')}
+                            onClick={() =>
+                              styleDispatch({
+                                type: 'SET_SIDER',
+                                payload: true,
+                              })
+                            }
+                          />
+                        ) : (
+                          <Button
+                            icon={<IconIndentLeft />}
+                            theme='light'
+                            aria-label={t('闭侧边栏')}
+                            onClick={() =>
+                              styleDispatch({
+                                type: 'SET_SIDER',
+                                payload: false,
+                              })
+                            }
+                          />
+                        )}
+                        {(isSelfUseMode || isDemoSiteMode) && (
+                          <Tag
+                            color={isSelfUseMode ? 'purple' : 'blue'}
+                            style={{
+                              position: 'absolute',
+                              top: '-8px',
+                              right: '-15px',
+                              fontSize: '0.7rem',
+                              padding: '0 4px',
+                              height: 'auto',
+                              lineHeight: '1.2',
+                              zIndex: 1,
+                              pointerEvents: 'none',
+                            }}
+                          >
+                            {isSelfUseMode ? t('自用模式') : t('演示站点')}
+                          </Tag>
+                        )}
+                      </div>
+                    ),
                   }
-                  {(isSelfUseMode || isDemoSiteMode) && (
-                    <Tag 
-                      color={isSelfUseMode ? 'purple' : 'blue'}
-                      style={{ 
-                        position: 'absolute',
-                        top: '-8px',
-                        right: '-15px',
-                        fontSize: '0.7rem',
-                        padding: '0 4px',
-                        height: 'auto',
-                        lineHeight: '1.2',
-                        zIndex: 1,
-                        pointerEvents: 'none'
-                      }}
-                    >
-                      {isSelfUseMode ? t('自用模式') : t('演示站点')}
-                    </Tag>
-                  )}
-                </div>
-              ),
-            }:{
-              logo: (
-                <div style={logoStyle}>
-                  <img src={logo} alt='logo' style={{ height: '28px' }} />
-                </div>
-              ),
-              text: (
-                <div style={{ position: 'relative', display: 'inline-block' }}>
-                  <span style={systemNameStyle}>{systemName}</span>
-                  {(isSelfUseMode || isDemoSiteMode) && (
-                    <Tag 
-                      color={isSelfUseMode ? 'purple' : 'blue'}
-                      style={{ 
-                        position: 'absolute', 
-                        top: '-10px', 
-                        right: '-25px', 
-                        fontSize: '0.7rem',
-                        padding: '0 4px',
-                        whiteSpace: 'nowrap',
-                        zIndex: 1,
-                        boxShadow: '0 0 3px rgba(255, 255, 255, 0.7)'
-                      }}
-                    >
-                      {isSelfUseMode ? t('自用模式') : t('演示站点')}
-                    </Tag>
-                  )}
-                </div>
-              ),
-            }}
+                : {
+                    logo: (
+                      <div style={logoStyle}>
+                        <img src={logo} alt='logo' style={{ height: '28px' }} />
+                      </div>
+                    ),
+                    text: (
+                      <div
+                        style={{
+                          position: 'relative',
+                          display: 'inline-block',
+                        }}
+                      >
+                        <span style={systemNameStyle}>{systemName}</span>
+                        {(isSelfUseMode || isDemoSiteMode) && (
+                          <Tag
+                            color={isSelfUseMode ? 'purple' : 'blue'}
+                            style={{
+                              position: 'absolute',
+                              top: '-10px',
+                              right: '-25px',
+                              fontSize: '0.7rem',
+                              padding: '0 4px',
+                              whiteSpace: 'nowrap',
+                              zIndex: 1,
+                              boxShadow: '0 0 3px rgba(255, 255, 255, 0.7)',
+                            }}
+                          >
+                            {isSelfUseMode ? t('自用模式') : t('演示站点')}
+                          </Tag>
+                        )}
+                      </div>
+                    ),
+                  }
+            }
             items={buttons}
             footer={
               <>
@@ -351,7 +404,7 @@ const HeaderBar = () => {
                 <>
                   <Switch
                     checkedText='🌞'
-                    size={styleState.isMobile?'default':'large'}
+                    size={styleState.isMobile ? 'default' : 'large'}
                     checked={theme === 'dark'}
                     uncheckedText='🌙'
                     style={switchStyle}
@@ -390,7 +443,9 @@ const HeaderBar = () => {
                       position='bottomRight'
                       render={
                         <Dropdown.Menu style={dropdownStyle}>
-                          <Dropdown.Item onClick={logout}>{t('退出')}</Dropdown.Item>
+                          <Dropdown.Item onClick={logout}>
+                            {t('退出')}
+                          </Dropdown.Item>
                         </Dropdown.Menu>
                       }
                     >
@@ -401,14 +456,18 @@ const HeaderBar = () => {
                       >
                         {userState.user.username[0]}
                       </Avatar>
-                      {styleState.isMobile?null:<Text style={{ marginLeft: '4px', fontWeight: '500' }}>{userState.user.username}</Text>}
+                      {styleState.isMobile ? null : (
+                        <Text style={{ marginLeft: '4px', fontWeight: '500' }}>
+                          {userState.user.username}
+                        </Text>
+                      )}
                     </Dropdown>
                   </>
                 ) : (
                   <>
                     <Nav.Item
                       itemKey={'login'}
-                      text={!styleState.isMobile?t('登录'):null}
+                      text={!styleState.isMobile ? t('登录') : null}
                       icon={<IconUser style={headerIconStyle} />}
                     />
                     {

+ 21 - 12
web/src/components/LoginForm.js

@@ -9,7 +9,11 @@ import {
   showSuccess,
   updateAPI,
 } from '../helpers';
-import {onGitHubOAuthClicked, onOIDCClicked, onLinuxDOOAuthClicked} from './utils';
+import {
+  onGitHubOAuthClicked,
+  onOIDCClicked,
+  onLinuxDOOAuthClicked,
+} from './utils';
 import Turnstile from 'react-turnstile';
 import {
   Button,
@@ -71,7 +75,6 @@ const LoginForm = () => {
     }
   }, []);
 
-
   const onWeChatLoginClicked = () => {
     setShowWeChatLoginModal(true);
   };
@@ -223,7 +226,8 @@ const LoginForm = () => {
                   }}
                 >
                   <Text>
-                    {t('没有账户?')} <Link to='/register'>{t('点击注册')}</Link>
+                    {t('没有账户?')}{' '}
+                    <Link to='/register'>{t('点击注册')}</Link>
                   </Text>
                   <Text>
                     {t('忘记密码?')} <Link to='/reset'>{t('点击重置')}</Link>
@@ -257,15 +261,18 @@ const LoginForm = () => {
                         <></>
                       )}
                       {status.oidc_enabled ? (
-                          <Button
-                              type='primary'
-                              icon={<OIDCIcon />}
-                              onClick={() =>
-                                  onOIDCClicked(status.oidc_authorization_endpoint, status.oidc_client_id)
-                              }
-                          />
+                        <Button
+                          type='primary'
+                          icon={<OIDCIcon />}
+                          onClick={() =>
+                            onOIDCClicked(
+                              status.oidc_authorization_endpoint,
+                              status.oidc_client_id,
+                            )
+                          }
+                        />
                       ) : (
-                          <></>
+                        <></>
                       )}
                       {status.linuxdo_oauth ? (
                         <Button
@@ -331,7 +338,9 @@ const LoginForm = () => {
                   </div>
                   <div style={{ textAlign: 'center' }}>
                     <p>
-                      {t('微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)')}
+                      {t(
+                        '微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)',
+                      )}
                     </p>
                   </div>
                   <Form size='large'>

+ 301 - 215
web/src/components/LogsTable.js

@@ -12,17 +12,19 @@ import {
 
 import {
   Avatar,
-  Button, Descriptions,
+  Button,
+  Descriptions,
   Form,
   Layout,
-  Modal, Popover,
+  Modal,
+  Popover,
   Select,
   Space,
   Spin,
   Table,
   Tag,
   Tooltip,
-  Checkbox
+  Checkbox,
 } from '@douyinfe/semi-ui';
 import { ITEMS_PER_PAGE } from '../constants';
 import {
@@ -36,7 +38,7 @@ import {
   renderModelPriceSimple,
   renderNumber,
   renderQuota,
-  stringToColor
+  stringToColor,
 } from '../helpers/render';
 import Paragraph from '@douyinfe/semi-ui/lib/es/typography/paragraph';
 import { getLogOther } from '../helpers/other.js';
@@ -78,23 +80,51 @@ const LogsTable = () => {
   function renderType(type) {
     switch (type) {
       case 1:
-        return <Tag color='cyan' size='large'>{t('充值')}</Tag>;
+        return (
+          <Tag color='cyan' size='large'>
+            {t('充值')}
+          </Tag>
+        );
       case 2:
-        return <Tag color='lime' size='large'>{t('消费')}</Tag>;
+        return (
+          <Tag color='lime' size='large'>
+            {t('消费')}
+          </Tag>
+        );
       case 3:
-        return <Tag color='orange' size='large'>{t('管理')}</Tag>;
+        return (
+          <Tag color='orange' size='large'>
+            {t('管理')}
+          </Tag>
+        );
       case 4:
-        return <Tag color='purple' size='large'>{t('系统')}</Tag>;
+        return (
+          <Tag color='purple' size='large'>
+            {t('系统')}
+          </Tag>
+        );
       default:
-        return <Tag color='black' size='large'>{t('未知')}</Tag>;
+        return (
+          <Tag color='black' size='large'>
+            {t('未知')}
+          </Tag>
+        );
     }
   }
 
   function renderIsStream(bool) {
     if (bool) {
-      return <Tag color='blue' size='large'>{t('流')}</Tag>;
+      return (
+        <Tag color='blue' size='large'>
+          {t('流')}
+        </Tag>
+      );
     } else {
-      return <Tag color='purple' size='large'>{t('非流')}</Tag>;
+      return (
+        <Tag color='purple' size='large'>
+          {t('非流')}
+        </Tag>
+      );
     }
   }
 
@@ -152,56 +182,70 @@ const LogsTable = () => {
   }
 
   function renderModelName(record) {
-
     let other = getLogOther(record.other);
-    let modelMapped = other?.is_model_mapped && other?.upstream_model_name && other?.upstream_model_name !== '';
+    let modelMapped =
+      other?.is_model_mapped &&
+      other?.upstream_model_name &&
+      other?.upstream_model_name !== '';
     if (!modelMapped) {
-      return <Tag
-        color={stringToColor(record.model_name)}
-        size='large'
-        onClick={(event) => {
-          copyText(event, record.model_name).then(r => {});
-        }}
-      >
-        {' '}{record.model_name}{' '}
-      </Tag>;
+      return (
+        <Tag
+          color={stringToColor(record.model_name)}
+          size='large'
+          onClick={(event) => {
+            copyText(event, record.model_name).then((r) => {});
+          }}
+        >
+          {' '}
+          {record.model_name}{' '}
+        </Tag>
+      );
     } else {
       return (
         <>
           <Space vertical align={'start'}>
-            <Popover content={
-              <div style={{padding: 10}}> 
-                <Space vertical align={'start'}>
-                  <Tag
-                    color={stringToColor(record.model_name)}
-                    size='large'
-                    onClick={(event) => {
-                      copyText(event, record.model_name).then(r => {});
-                    }}
-                  >
-                    {t('请求并计费模型')}{' '}{record.model_name}{' '}
-                  </Tag>
-                  <Tag
-                    color={stringToColor(other.upstream_model_name)}
-                    size='large'
-                    onClick={(event) => {
-                      copyText(event, other.upstream_model_name).then(r => {});
-                    }}
-                  >
-                    {t('实际模型')}{' '}{other.upstream_model_name}{' '}
-                  </Tag>
-                </Space>
-              </div>
-            }>
+            <Popover
+              content={
+                <div style={{ padding: 10 }}>
+                  <Space vertical align={'start'}>
+                    <Tag
+                      color={stringToColor(record.model_name)}
+                      size='large'
+                      onClick={(event) => {
+                        copyText(event, record.model_name).then((r) => {});
+                      }}
+                    >
+                      {t('请求并计费模型')} {record.model_name}{' '}
+                    </Tag>
+                    <Tag
+                      color={stringToColor(other.upstream_model_name)}
+                      size='large'
+                      onClick={(event) => {
+                        copyText(event, other.upstream_model_name).then(
+                          (r) => {},
+                        );
+                      }}
+                    >
+                      {t('实际模型')} {other.upstream_model_name}{' '}
+                    </Tag>
+                  </Space>
+                </div>
+              }
+            >
               <Tag
                 color={stringToColor(record.model_name)}
                 size='large'
                 onClick={(event) => {
-                  copyText(event, record.model_name).then(r => {});
+                  copyText(event, record.model_name).then((r) => {});
                 }}
-                suffixIcon={<IconRefresh style={{width: '0.8em', height: '0.8em', opacity: 0.6}} />}
+                suffixIcon={
+                  <IconRefresh
+                    style={{ width: '0.8em', height: '0.8em', opacity: 0.6 }}
+                  />
+                }
               >
-                {' '}{record.model_name}{' '}
+                {' '}
+                {record.model_name}{' '}
               </Tag>
             </Popover>
             {/*<Tooltip content={t('实际模型')}>*/}
@@ -219,7 +263,6 @@ const LogsTable = () => {
         </>
       );
     }
-
   }
 
   // Define column keys for selection
@@ -236,7 +279,7 @@ const LogsTable = () => {
     COMPLETION: 'completion',
     COST: 'cost',
     RETRY: 'retry',
-    DETAILS: 'details'
+    DETAILS: 'details',
   };
 
   // State for column visibility
@@ -277,7 +320,7 @@ const LogsTable = () => {
       [COLUMN_KEYS.COMPLETION]: true,
       [COLUMN_KEYS.COST]: true,
       [COLUMN_KEYS.RETRY]: isAdminUser,
-      [COLUMN_KEYS.DETAILS]: true
+      [COLUMN_KEYS.DETAILS]: true,
     };
   };
 
@@ -296,18 +339,23 @@ const LogsTable = () => {
 
   // Handle "Select All" checkbox
   const handleSelectAll = (checked) => {
-    const allKeys = Object.keys(COLUMN_KEYS).map(key => COLUMN_KEYS[key]);
+    const allKeys = Object.keys(COLUMN_KEYS).map((key) => COLUMN_KEYS[key]);
     const updatedColumns = {};
-    
-    allKeys.forEach(key => {
+
+    allKeys.forEach((key) => {
       // For admin-only columns, only enable them if user is admin
-      if ((key === COLUMN_KEYS.CHANNEL || key === COLUMN_KEYS.USERNAME || key === COLUMN_KEYS.RETRY) && !isAdminUser) {
+      if (
+        (key === COLUMN_KEYS.CHANNEL ||
+          key === COLUMN_KEYS.USERNAME ||
+          key === COLUMN_KEYS.RETRY) &&
+        !isAdminUser
+      ) {
         updatedColumns[key] = false;
       } else {
         updatedColumns[key] = checked;
       }
     });
-    
+
     setVisibleColumns(updatedColumns);
   };
 
@@ -361,7 +409,7 @@ const LogsTable = () => {
               style={{ marginRight: 4 }}
               onClick={(event) => {
                 event.stopPropagation();
-                showUserInfo(record.user_id)
+                showUserInfo(record.user_id);
               }}
             >
               {typeof text === 'string' && text.slice(0, 1)}
@@ -403,32 +451,27 @@ const LogsTable = () => {
       dataIndex: 'group',
       render: (text, record, index) => {
         if (record.type === 0 || record.type === 2) {
-         if (record.group) {
-            return (
-              <>
-                {renderGroup(record.group)}
-              </>
-            );
-         } else {
-           let other = null;
-           try {
-             other = JSON.parse(record.other);
-           } catch (e) {
-             console.error(`Failed to parse record.other: "${record.other}".`, e);
-           }
-           if (other === null) {
-             return <></>;
-           }
-           if (other.group !== undefined) {
-             return (
-               <>
-                 {renderGroup(other.group)}
-               </>
-             );
-           } else {
-             return <></>;
-           }
-         }
+          if (record.group) {
+            return <>{renderGroup(record.group)}</>;
+          } else {
+            let other = null;
+            try {
+              other = JSON.parse(record.other);
+            } catch (e) {
+              console.error(
+                `Failed to parse record.other: "${record.other}".`,
+                e,
+              );
+            }
+            if (other === null) {
+              return <></>;
+            }
+            if (other.group !== undefined) {
+              return <>{renderGroup(other.group)}</>;
+            } else {
+              return <></>;
+            }
+          }
         } else {
           return <></>;
         }
@@ -572,30 +615,30 @@ const LogsTable = () => {
 
         let content = other?.claude
           ? renderClaudeModelPriceSimple(
-            other.model_ratio,
-            other.model_price,
-            other.group_ratio,
-            other.cache_tokens || 0,
-            other.cache_ratio || 1.0,
-            other.cache_creation_tokens || 0,
-            other.cache_creation_ratio || 1.0,
-          )
+              other.model_ratio,
+              other.model_price,
+              other.group_ratio,
+              other.cache_tokens || 0,
+              other.cache_ratio || 1.0,
+              other.cache_creation_tokens || 0,
+              other.cache_creation_ratio || 1.0,
+            )
           : renderModelPriceSimple(
-            other.model_ratio,
-            other.model_price,
-            other.group_ratio,
-            other.cache_tokens || 0,
-            other.cache_ratio || 1.0,
-          );
+              other.model_ratio,
+              other.model_price,
+              other.group_ratio,
+              other.cache_tokens || 0,
+              other.cache_ratio || 1.0,
+            );
         return (
-            <Paragraph
-                ellipsis={{
-                  rows: 2,
-                }}
-                style={{ maxWidth: 240 }}
-            >
-              {content}
-            </Paragraph>
+          <Paragraph
+            ellipsis={{
+              rows: 2,
+            }}
+            style={{ maxWidth: 240 }}
+          >
+            {content}
+          </Paragraph>
         );
       },
     },
@@ -605,13 +648,16 @@ const LogsTable = () => {
   useEffect(() => {
     if (Object.keys(visibleColumns).length > 0) {
       // Save to localStorage
-      localStorage.setItem('logs-table-columns', JSON.stringify(visibleColumns));
+      localStorage.setItem(
+        'logs-table-columns',
+        JSON.stringify(visibleColumns),
+      );
     }
   }, [visibleColumns]);
 
   // Filter columns based on visibility settings
   const getVisibleColumns = () => {
-    return allColumns.filter(column => visibleColumns[column.key]);
+    return allColumns.filter((column) => visibleColumns[column.key]);
   };
 
   // Column selector modal
@@ -624,42 +670,59 @@ const LogsTable = () => {
         footer={
           <>
             <Button onClick={() => initDefaultColumns()}>{t('重置')}</Button>
-            <Button onClick={() => setShowColumnSelector(false)}>{t('取消')}</Button>
-            <Button type="primary" onClick={() => setShowColumnSelector(false)}>{t('确定')}</Button>
+            <Button onClick={() => setShowColumnSelector(false)}>
+              {t('取消')}
+            </Button>
+            <Button type='primary' onClick={() => setShowColumnSelector(false)}>
+              {t('确定')}
+            </Button>
           </>
         }
       >
         <div style={{ marginBottom: 20 }}>
           <Checkbox
-            checked={Object.values(visibleColumns).every(v => v === true)}
-            indeterminate={Object.values(visibleColumns).some(v => v === true) && !Object.values(visibleColumns).every(v => v === true)}
-            onChange={e => handleSelectAll(e.target.checked)}
+            checked={Object.values(visibleColumns).every((v) => v === true)}
+            indeterminate={
+              Object.values(visibleColumns).some((v) => v === true) &&
+              !Object.values(visibleColumns).every((v) => v === true)
+            }
+            onChange={(e) => handleSelectAll(e.target.checked)}
           >
             {t('全选')}
           </Checkbox>
         </div>
-        <div style={{ 
-          display: 'flex', 
-          flexWrap: 'wrap',
-          maxHeight: '400px',
-          overflowY: 'auto',
-          border: '1px solid var(--semi-color-border)',
-          borderRadius: '6px',
-          padding: '16px'
-        }}>
-          {allColumns.map(column => {
+        <div
+          style={{
+            display: 'flex',
+            flexWrap: 'wrap',
+            maxHeight: '400px',
+            overflowY: 'auto',
+            border: '1px solid var(--semi-color-border)',
+            borderRadius: '6px',
+            padding: '16px',
+          }}
+        >
+          {allColumns.map((column) => {
             // Skip admin-only columns for non-admin users
-            if (!isAdminUser && (column.key === COLUMN_KEYS.CHANNEL || 
-                                column.key === COLUMN_KEYS.USERNAME || 
-                                column.key === COLUMN_KEYS.RETRY)) {
+            if (
+              !isAdminUser &&
+              (column.key === COLUMN_KEYS.CHANNEL ||
+                column.key === COLUMN_KEYS.USERNAME ||
+                column.key === COLUMN_KEYS.RETRY)
+            ) {
               return null;
             }
-            
+
             return (
-              <div key={column.key} style={{ width: '50%', marginBottom: 16, paddingRight: 8 }}>
+              <div
+                key={column.key}
+                style={{ width: '50%', marginBottom: 16, paddingRight: 8 }}
+              >
                 <Checkbox
                   checked={!!visibleColumns[column.key]}
-                  onChange={e => handleColumnVisibilityChange(column.key, e.target.checked)}
+                  onChange={(e) =>
+                    handleColumnVisibilityChange(column.key, e.target.checked)
+                  }
                 >
                   {column.title}
                 </Checkbox>
@@ -709,7 +772,7 @@ const LogsTable = () => {
   });
 
   const handleInputChange = (value, name) => {
-    setInputs(inputs => ({ ...inputs, [name]: value }));
+    setInputs((inputs) => ({ ...inputs, [name]: value }));
   };
 
   const getLogSelfStat = async () => {
@@ -765,10 +828,18 @@ const LogsTable = () => {
         title: t('用户信息'),
         content: (
           <div style={{ padding: 12 }}>
-            <p>{t('用户名')}: {data.username}</p>
-            <p>{t('余额')}: {renderQuota(data.quota)}</p>
-            <p>{t('已用额度')}:{renderQuota(data.used_quota)}</p>
-            <p>{t('请求次数')}:{renderNumber(data.request_count)}</p>
+            <p>
+              {t('用户名')}: {data.username}
+            </p>
+            <p>
+              {t('余额')}: {renderQuota(data.quota)}
+            </p>
+            <p>
+              {t('已用额度')}:{renderQuota(data.used_quota)}
+            </p>
+            <p>
+              {t('请求次数')}:{renderNumber(data.request_count)}
+            </p>
           </div>
         ),
         centered: true,
@@ -803,11 +874,11 @@ const LogsTable = () => {
         //   key: '渠道重试',
         //   value: content,
         // })
-      }      
+      }
       if (isAdminUser && (logs[i].type === 0 || logs[i].type === 2)) {
         expandDataLocal.push({
           key: t('渠道信息'),
-          value: `${logs[i].channel} - ${logs[i].channel_name || '[未知]'}`
+          value: `${logs[i].channel} - ${logs[i].channel_name || '[未知]'}`,
         });
       }
       if (other?.ws || other?.audio) {
@@ -845,25 +916,28 @@ const LogsTable = () => {
           key: t('日志详情'),
           value: other?.claude
             ? renderClaudeLogContent(
-              other?.model_ratio,
-              other.completion_ratio,
-              other.model_price,
-              other.group_ratio,
-              other.user_group_ratio,
-              other.cache_ratio || 1.0,
-              other.cache_creation_ratio || 1.0
-            )
+                other?.model_ratio,
+                other.completion_ratio,
+                other.model_price,
+                other.group_ratio,
+                other.user_group_ratio,
+                other.cache_ratio || 1.0,
+                other.cache_creation_ratio || 1.0,
+              )
             : renderLogContent(
-              other?.model_ratio,
-              other.completion_ratio,
-              other.model_price,
-              other.group_ratio,
-              other.user_group_ratio
-            ),
+                other?.model_ratio,
+                other.completion_ratio,
+                other.model_price,
+                other.group_ratio,
+                other.user_group_ratio,
+              ),
         });
       }
       if (logs[i].type === 2) {
-        let modelMapped = other?.is_model_mapped && other?.upstream_model_name && other?.upstream_model_name !== '';
+        let modelMapped =
+          other?.is_model_mapped &&
+          other?.upstream_model_name &&
+          other?.upstream_model_name !== '';
         if (modelMapped) {
           expandDataLocal.push({
             key: t('请求并计费模型'),
@@ -1014,29 +1088,41 @@ const LogsTable = () => {
         <Header>
           <Spin spinning={loadingStat}>
             <Space>
-              <Tag color='blue' size='large' style={{ 
-                padding: 15, 
-                borderRadius: '8px', 
-                fontWeight: 500,
-                boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)'
-              }}>
+              <Tag
+                color='blue'
+                size='large'
+                style={{
+                  padding: 15,
+                  borderRadius: '8px',
+                  fontWeight: 500,
+                  boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
+                }}
+              >
                 {t('消耗额度')}: {renderQuota(stat.quota)}
               </Tag>
-              <Tag color='pink' size='large' style={{ 
-                padding: 15, 
-                borderRadius: '8px', 
-                fontWeight: 500,
-                boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)'
-              }}>
+              <Tag
+                color='pink'
+                size='large'
+                style={{
+                  padding: 15,
+                  borderRadius: '8px',
+                  fontWeight: 500,
+                  boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
+                }}
+              >
                 RPM: {stat.rpm}
               </Tag>
-              <Tag color='white' size='large' style={{ 
-                padding: 15, 
-                border: 'none', 
-                boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)', 
-                borderRadius: '8px',
-                fontWeight: 500,
-              }}>
+              <Tag
+                color='white'
+                size='large'
+                style={{
+                  padding: 15,
+                  border: 'none',
+                  boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
+                  borderRadius: '8px',
+                  fontWeight: 500,
+                }}
+              >
                 TPM: {stat.tpm}
               </Tag>
             </Space>
@@ -1046,46 +1132,46 @@ const LogsTable = () => {
           <>
             <Form.Section>
               <div style={{ marginBottom: 10 }}>
-              {
-                  styleState.isMobile ? (
-                    <div>
-                      <Form.DatePicker
-                        field='start_timestamp'
-                        label={t('起始时间')}
-                        style={{ width: 272 }}
-                        initValue={start_timestamp}
-                        type='dateTime'
-                        onChange={(value) => {
-                          console.log(value);
-                          handleInputChange(value, 'start_timestamp')
-                        }}
-                      />
-                      <Form.DatePicker
-                        field='end_timestamp'
-                        fluid
-                        label={t('结束时间')}
-                        style={{ width: 272 }}
-                        initValue={end_timestamp}
-                        type='dateTime'
-                        onChange={(value) => handleInputChange(value, 'end_timestamp')}
-                      />
-                    </div>
-                  ) : (
+                {styleState.isMobile ? (
+                  <div>
                     <Form.DatePicker
-                      field="range_timestamp"
-                      label={t('时间范围')}
-                      initValue={[start_timestamp, end_timestamp]}
-                      type="dateTimeRange"
-                      name="range_timestamp"
+                      field='start_timestamp'
+                      label={t('起始时间')}
+                      style={{ width: 272 }}
+                      initValue={start_timestamp}
+                      type='dateTime'
                       onChange={(value) => {
-                        if (Array.isArray(value) && value.length === 2) {
-                          handleInputChange(value[0], 'start_timestamp');
-                          handleInputChange(value[1], 'end_timestamp');
-                        }
+                        console.log(value);
+                        handleInputChange(value, 'start_timestamp');
                       }}
                     />
-                  )
-                }
+                    <Form.DatePicker
+                      field='end_timestamp'
+                      fluid
+                      label={t('结束时间')}
+                      style={{ width: 272 }}
+                      initValue={end_timestamp}
+                      type='dateTime'
+                      onChange={(value) =>
+                        handleInputChange(value, 'end_timestamp')
+                      }
+                    />
+                  </div>
+                ) : (
+                  <Form.DatePicker
+                    field='range_timestamp'
+                    label={t('时间范围')}
+                    initValue={[start_timestamp, end_timestamp]}
+                    type='dateTimeRange'
+                    name='range_timestamp'
+                    onChange={(value) => {
+                      if (Array.isArray(value) && value.length === 2) {
+                        handleInputChange(value[0], 'start_timestamp');
+                        handleInputChange(value[1], 'end_timestamp');
+                      }
+                    }}
+                  />
+                )}
               </div>
             </Form.Section>
             <Form.Input
@@ -1146,14 +1232,14 @@ const LogsTable = () => {
             <Form.Section></Form.Section>
           </>
         </Form>
-        <div style={{marginTop:10}}>
+        <div style={{ marginTop: 10 }}>
           <Select
-              defaultValue='0'
-              style={{ width: 120 }}
-              onChange={(value) => {
-                setLogType(parseInt(value));
-                loadLogs(0, pageSize, parseInt(value));
-              }}
+            defaultValue='0'
+            style={{ width: 120 }}
+            onChange={(value) => {
+              setLogType(parseInt(value));
+              loadLogs(0, pageSize, parseInt(value));
+            }}
           >
             <Select.Option value='0'>{t('全部')}</Select.Option>
             <Select.Option value='1'>{t('充值')}</Select.Option>
@@ -1177,13 +1263,13 @@ const LogsTable = () => {
           expandedRowRender={expandRowRender}
           expandRowByClick={true}
           dataSource={logs}
-          rowKey="key"
+          rowKey='key'
           pagination={{
             formatPageText: (page) =>
               t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
                 start: page.currentStart,
                 end: page.currentEnd,
-                total: logCount
+                total: logCount,
               }),
             currentPage: activePage,
             pageSize: pageSize,

+ 13 - 15
web/src/components/MjLogsTable.js

@@ -46,7 +46,6 @@ const LogsTable = () => {
   const [isModalOpen, setIsModalOpen] = useState(false);
   const [modalContent, setModalContent] = useState('');
   function renderType(type) {
-    
     switch (type) {
       case 'IMAGINE':
         return (
@@ -98,9 +97,9 @@ const LogsTable = () => {
         );
       case 'UPLOAD':
         return (
-            <Tag color='blue' size='large'>
-              上传文件
-            </Tag>
+          <Tag color='blue' size='large'>
+            上传文件
+          </Tag>
         );
       case 'SHORTEN':
         return (
@@ -152,9 +151,8 @@ const LogsTable = () => {
         );
     }
   }
-  
+
   function renderCode(code) {
-    
     switch (code) {
       case 1:
         return (
@@ -188,9 +186,8 @@ const LogsTable = () => {
         );
     }
   }
-  
+
   function renderStatus(type) {
-    
     switch (type) {
       case 'SUCCESS':
         return (
@@ -236,22 +233,21 @@ const LogsTable = () => {
         );
     }
   }
-  
+
   const renderTimestamp = (timestampInSeconds) => {
     const date = new Date(timestampInSeconds * 1000); // 从秒转换为毫秒
-  
+
     const year = date.getFullYear(); // 获取年份
     const month = ('0' + (date.getMonth() + 1)).slice(-2); // 获取月份,从0开始需要+1,并保证两位数
     const day = ('0' + date.getDate()).slice(-2); // 获取日期,并保证两位数
     const hours = ('0' + date.getHours()).slice(-2); // 获取小时,并保证两位数
     const minutes = ('0' + date.getMinutes()).slice(-2); // 获取分钟,并保证两位数
     const seconds = ('0' + date.getSeconds()).slice(-2); // 获取秒钟,并保证两位数
-  
+
     return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; // 格式化输出
   };
   // 修改renderDuration函数以包含颜色逻辑
   function renderDuration(submit_time, finishTime) {
-    
     if (!submit_time || !finishTime) return 'N/A';
 
     const start = new Date(submit_time);
@@ -261,7 +257,7 @@ const LogsTable = () => {
     const color = durationSec > 60 ? 'red' : 'green';
 
     return (
-      <Tag color={color} size="large">
+      <Tag color={color} size='large'>
         {durationSec} {t('秒')}
       </Tag>
     );
@@ -560,7 +556,9 @@ const LogsTable = () => {
         {isAdminUser && showBanner ? (
           <Banner
             type='info'
-            description={t('当前未开启Midjourney回调,部分项目可能无法获得绘图结果,可在运营设置中开启。')}
+            description={t(
+              '当前未开启Midjourney回调,部分项目可能无法获得绘图结果,可在运营设置中开启。',
+            )}
           />
         ) : (
           <></>
@@ -634,7 +632,7 @@ const LogsTable = () => {
               t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
                 start: page.currentStart,
                 end: page.currentEnd,
-                total: logCount
+                total: logCount,
               }),
           }}
           loading={loading}

+ 73 - 52
web/src/components/ModelPricing.js

@@ -34,12 +34,12 @@ const ModelPricing = () => {
   const [selectedGroup, setSelectedGroup] = useState('default');
 
   const rowSelection = useMemo(
-      () => ({
-          onChange: (selectedRowKeys, selectedRows) => {
-            setSelectedRowKeys(selectedRowKeys);
-          },
-      }),
-      []
+    () => ({
+      onChange: (selectedRowKeys, selectedRows) => {
+        setSelectedRowKeys(selectedRowKeys);
+      },
+    }),
+    [],
   );
 
   const handleChange = (value) => {
@@ -59,7 +59,7 @@ const ModelPricing = () => {
     const newFilteredValue = value ? [value] : [];
     setFilteredValue(newFilteredValue);
   };
-  
+
   function renderQuotaType(type) {
     // Ensure all cases are string literals by adding quotes.
     switch (type) {
@@ -79,7 +79,7 @@ const ModelPricing = () => {
         return t('未知');
     }
   }
-  
+
   function renderAvailable(available) {
     return (
       <Popover
@@ -96,9 +96,9 @@ const ModelPricing = () => {
           borderStyle: 'solid',
         }}
       >
-        <IconVerify style={{ color: 'green' }}  size="large" />
+        <IconVerify style={{ color: 'green' }} size='large' />
       </Popover>
-    )
+    );
   }
 
   const columns = [
@@ -106,7 +106,7 @@ const ModelPricing = () => {
       title: t('可用性'),
       dataIndex: 'available',
       render: (text, record, index) => {
-         // if record.enable_groups contains selectedGroup, then available is true
+        // if record.enable_groups contains selectedGroup, then available is true
         return renderAvailable(record.enable_groups.includes(selectedGroup));
       },
       sorter: (a, b) => a.available - b.available,
@@ -145,7 +145,6 @@ const ModelPricing = () => {
       title: t('可用分组'),
       dataIndex: 'enable_groups',
       render: (text, record, index) => {
-        
         // enable_groups is a string array
         return (
           <Space>
@@ -153,11 +152,7 @@ const ModelPricing = () => {
               if (usableGroup[group]) {
                 if (group === selectedGroup) {
                   return (
-                    <Tag
-                      color='blue'
-                      size='large'
-                      prefixIcon={<IconVerify />}
-                    >
+                    <Tag color='blue' size='large' prefixIcon={<IconVerify />}>
                       {group}
                     </Tag>
                   );
@@ -168,10 +163,12 @@ const ModelPricing = () => {
                       size='large'
                       onClick={() => {
                         setSelectedGroup(group);
-                        showInfo(t('当前查看的分组为:{{group}},倍率为:{{ratio}}', {
-                          group: group,
-                          ratio: groupRatio[group]
-                        }));
+                        showInfo(
+                          t('当前查看的分组为:{{group}},倍率为:{{ratio}}', {
+                            group: group,
+                            ratio: groupRatio[group],
+                          }),
+                        );
                       }}
                     >
                       {group}
@@ -186,22 +183,23 @@ const ModelPricing = () => {
     },
     {
       title: () => (
-        <span style={{'display':'flex','alignItems':'center'}}>
+        <span style={{ display: 'flex', alignItems: 'center' }}>
           {t('倍率')}
           <Popover
             content={
               <div style={{ padding: 8 }}>
-                {t('倍率是为了方便换算不同价格的模型')}<br/>
+                {t('倍率是为了方便换算不同价格的模型')}
+                <br />
                 {t('点击查看倍率说明')}
               </div>
             }
             position='top'
             style={{
-                backgroundColor: 'rgba(var(--semi-blue-4),1)',
-                borderColor: 'rgba(var(--semi-blue-4),1)',
-                color: 'var(--semi-color-white)',
-                borderWidth: 1,
-                borderStyle: 'solid',
+              backgroundColor: 'rgba(var(--semi-blue-4),1)',
+              borderColor: 'rgba(var(--semi-blue-4),1)',
+              color: 'var(--semi-color-white)',
+              borderWidth: 1,
+              borderStyle: 'solid',
             }}
           >
             <IconHelpCircle
@@ -219,11 +217,18 @@ const ModelPricing = () => {
         let completionRatio = parseFloat(record.completion_ratio.toFixed(3));
         content = (
           <>
-            <Text>{t('模型倍率')}:{record.quota_type === 0 ? text : t('无')}</Text>
+            <Text>
+              {t('模型倍率')}:{record.quota_type === 0 ? text : t('无')}
+            </Text>
             <br />
-            <Text>{t('补全倍率')}:{record.quota_type === 0 ? completionRatio : t('无')}</Text>
+            <Text>
+              {t('补全倍率')}:
+              {record.quota_type === 0 ? completionRatio : t('无')}
+            </Text>
             <br />
-            <Text>{t('分组倍率')}:{groupRatio[selectedGroup]}</Text>
+            <Text>
+              {t('分组倍率')}:{groupRatio[selectedGroup]}
+            </Text>
           </>
         );
         return <div>{content}</div>;
@@ -236,21 +241,31 @@ const ModelPricing = () => {
         let content = text;
         if (record.quota_type === 0) {
           // 这里的 *2 是因为 1倍率=0.002刀,请勿删除
-          let inputRatioPrice = record.model_ratio * 2 * groupRatio[selectedGroup];
+          let inputRatioPrice =
+            record.model_ratio * 2 * groupRatio[selectedGroup];
           let completionRatioPrice =
             record.model_ratio *
-            record.completion_ratio * 2 *
+            record.completion_ratio *
+            2 *
             groupRatio[selectedGroup];
           content = (
             <>
-              <Text>{t('提示')} ${inputRatioPrice} / 1M tokens</Text>
+              <Text>
+                {t('提示')} ${inputRatioPrice} / 1M tokens
+              </Text>
               <br />
-              <Text>{t('补全')} ${completionRatioPrice} / 1M tokens</Text>
+              <Text>
+                {t('补全')} ${completionRatioPrice} / 1M tokens
+              </Text>
             </>
           );
         } else {
           let price = parseFloat(text) * groupRatio[selectedGroup];
-          content = <>${t('模型价格')}:${price}</>;
+          content = (
+            <>
+              ${t('模型价格')}:${price}
+            </>
+          );
         }
         return <div>{content}</div>;
       },
@@ -300,7 +315,7 @@ const ModelPricing = () => {
     if (success) {
       setGroupRatio(group_ratio);
       setUsableGroup(usable_group);
-      setSelectedGroup(userState.user ? userState.user.group : 'default')
+      setSelectedGroup(userState.user ? userState.user.group : 'default');
       setModelsFormat(data, group_ratio);
     } else {
       showError(message);
@@ -330,32 +345,38 @@ const ModelPricing = () => {
       <Layout>
         {userState.user ? (
           <Banner
-            type="success"
+            type='success'
             fullMode={false}
-            closeIcon="null"
+            closeIcon='null'
             description={t('您的默认分组为:{{group}},分组倍率为:{{ratio}}', {
               group: userState.user.group,
-              ratio: groupRatio[userState.user.group]
+              ratio: groupRatio[userState.user.group],
             })}
           />
         ) : (
           <Banner
             type='warning'
             fullMode={false}
-            closeIcon="null"
+            closeIcon='null'
             description={t('您还未登陆,显示的价格为默认分组倍率: {{ratio}}', {
-              ratio: groupRatio['default']
+              ratio: groupRatio['default'],
             })}
           />
         )}
-        <br/>
-        <Banner 
-            type="info"
-            fullMode={false}
-            description={<div>{t('按量计费费用 = 分组倍率 × 模型倍率 × (提示token数 + 补全token数 × 补全倍率)/ 500000 (单位:美元)')}</div>}
-            closeIcon="null"
+        <br />
+        <Banner
+          type='info'
+          fullMode={false}
+          description={
+            <div>
+              {t(
+                '按量计费费用 = 分组倍率 × 模型倍率 × (提示token数 + 补全token数 × 补全倍率)/ 500000 (单位:美元)',
+              )}
+            </div>
+          }
+          closeIcon='null'
         />
-        <br/>
+        <br />
         <Space style={{ marginBottom: 16 }}>
           <Input
             placeholder={t('模糊搜索模型名称')}
@@ -368,11 +389,11 @@ const ModelPricing = () => {
           <Button
             theme='light'
             type='tertiary'
-            style={{width: 150}}
+            style={{ width: 150 }}
             onClick={() => {
               copyText(selectedRowKeys);
             }}
-            disabled={selectedRowKeys == ""}
+            disabled={selectedRowKeys == ''}
           >
             {t('复制选中模型')}
           </Button>
@@ -387,7 +408,7 @@ const ModelPricing = () => {
               t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
                 start: page.currentStart,
                 end: page.currentEnd,
-                total: models.length
+                total: models.length,
               }),
             pageSize: models.length,
             showSizeChanger: false,

+ 3 - 6
web/src/components/ModelSetting.js

@@ -1,7 +1,6 @@
 import React, { useEffect, useState } from 'react';
 import { Card, Spin, Tabs } from '@douyinfe/semi-ui';
 
-
 import { API, showError, showSuccess } from '../helpers';
 import { useTranslation } from 'react-i18next';
 import SettingGeminiModel from '../pages/Setting/Model/SettingGeminiModel.js';
@@ -34,15 +33,13 @@ const ModelSetting = () => {
         if (
           item.key === 'gemini.safety_settings' ||
           item.key === 'gemini.version_settings' ||
-          item.key === 'claude.model_headers_settings'||
-          item.key === 'claude.default_max_tokens'||
+          item.key === 'claude.model_headers_settings' ||
+          item.key === 'claude.default_max_tokens' ||
           item.key === 'gemini.supported_imagine_models'
         ) {
           item.value = JSON.stringify(JSON.parse(item.value), null, 2);
         }
-        if (
-          item.key.endsWith('Enabled') || item.key.endsWith('enabled')
-        ) {
+        if (item.key.endsWith('Enabled') || item.key.endsWith('enabled')) {
           newInputs[item.key] = item.value === 'true' ? true : false;
         } else {
           newInputs[item.key] = item.value;

+ 47 - 45
web/src/components/OAuth2Callback.js

@@ -6,56 +6,58 @@ import { UserContext } from '../context/User';
 import { setUserData } from '../helpers/data.js';
 
 const OAuth2Callback = (props) => {
-    const [searchParams, setSearchParams] = useSearchParams();
+  const [searchParams, setSearchParams] = useSearchParams();
 
-    const [userState, userDispatch] = useContext(UserContext);
-    const [prompt, setPrompt] = useState('处理中...');
-    const [processing, setProcessing] = useState(true);
+  const [userState, userDispatch] = useContext(UserContext);
+  const [prompt, setPrompt] = useState('处理中...');
+  const [processing, setProcessing] = useState(true);
 
-    let navigate = useNavigate();
+  let navigate = useNavigate();
 
-    const sendCode = async (code, state, count) => {
-        const res = await API.get(`/api/oauth/${props.type}?code=${code}&state=${state}`);
-        const { success, message, data } = res.data;
-        if (success) {
-            if (message === 'bind') {
-                showSuccess('绑定成功!');
-                navigate('/setting');
-            } else {
-                userDispatch({ type: 'login', payload: data });
-                localStorage.setItem('user', JSON.stringify(data));
-                setUserData(data);
-                updateAPI()
-                showSuccess('登录成功!');
-                navigate('/token');
-            }
-        } else {
-            showError(message);
-            if (count === 0) {
-                setPrompt(`操作失败,重定向至登录界面中...`);
-                navigate('/setting'); // in case this is failed to bind GitHub
-                return;
-            }
-            count++;
-            setPrompt(`出现错误,第 ${count} 次重试中...`);
-            await new Promise((resolve) => setTimeout(resolve, count * 2000));
-            await sendCode(code, state, count);
-        }
-    };
+  const sendCode = async (code, state, count) => {
+    const res = await API.get(
+      `/api/oauth/${props.type}?code=${code}&state=${state}`,
+    );
+    const { success, message, data } = res.data;
+    if (success) {
+      if (message === 'bind') {
+        showSuccess('绑定成功!');
+        navigate('/setting');
+      } else {
+        userDispatch({ type: 'login', payload: data });
+        localStorage.setItem('user', JSON.stringify(data));
+        setUserData(data);
+        updateAPI();
+        showSuccess('登录成功!');
+        navigate('/token');
+      }
+    } else {
+      showError(message);
+      if (count === 0) {
+        setPrompt(`操作失败,重定向至登录界面中...`);
+        navigate('/setting'); // in case this is failed to bind GitHub
+        return;
+      }
+      count++;
+      setPrompt(`出现错误,第 ${count} 次重试中...`);
+      await new Promise((resolve) => setTimeout(resolve, count * 2000));
+      await sendCode(code, state, count);
+    }
+  };
 
-    useEffect(() => {
-        let code = searchParams.get('code');
-        let state = searchParams.get('state');
-        sendCode(code, state, 0).then();
-    }, []);
+  useEffect(() => {
+    let code = searchParams.get('code');
+    let state = searchParams.get('state');
+    sendCode(code, state, 0).then();
+  }, []);
 
-    return (
-        <Segment style={{ minHeight: '300px' }}>
-            <Dimmer active inverted>
-                <Loader size='large'>{prompt}</Loader>
-            </Dimmer>
-        </Segment>
-    );
+  return (
+    <Segment style={{ minHeight: '300px' }}>
+      <Dimmer active inverted>
+        <Loader size='large'>{prompt}</Loader>
+      </Dimmer>
+    </Segment>
+  );
 };
 
 export default OAuth2Callback;

+ 31 - 15
web/src/components/OIDCIcon.js

@@ -2,21 +2,37 @@ import React from 'react';
 import { Icon } from '@douyinfe/semi-ui';
 
 const OIDCIcon = (props) => {
-    function CustomIcon() {
-        return (
-            <svg t="1723135116886" className="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
-                 p-id="10969" width="1em" height="1em">
-                <path
-                    d="M512 960C265 960 64 759 64 512S265 64 512 64s448 201 448 448-201 448-448 448z m0-882.6c-239.7 0-434.6 195-434.6 434.6s195 434.6 434.6 434.6 434.6-195 434.6-434.6S751.7 77.4 512 77.4z"
-                    p-id="10970" fill="#2c2c2c" stroke="#2c2c2c" stroke-width="60"></path>
-                <path
-                    d="M197.7 512c0-78.3 31.6-98.8 87.2-98.8 56.2 0 87.2 20.5 87.2 98.8s-31 98.8-87.2 98.8c-55.7 0-87.2-20.5-87.2-98.8z m130.4 0c0-46.8-7.8-64.5-43.2-64.5-35.2 0-42.9 17.7-42.9 64.5 0 47.1 7.8 63.7 42.9 63.7 35.4 0 43.2-16.6 43.2-63.7zM409.7 415.9h42.1V608h-42.1V415.9zM653.9 512c0 74.2-37.1 96.1-93.6 96.1h-65.9V415.9h65.9c56.5 0 93.6 16.1 93.6 96.1z m-43.5 0c0-49.3-17.7-60.6-52.3-60.6h-21.6v120.7h21.6c35.4 0 52.3-13.3 52.3-60.1zM686.5 512c0-74.2 36.3-98.8 92.7-98.8 18.3 0 33.2 2.2 44.8 6.4v36.3c-11.9-4.2-26-6.6-42.1-6.6-34.6 0-49.8 15.5-49.8 62.6 0 50.1 15.2 62.6 49.3 62.6 15.8 0 30.2-2.2 44.8-7.5v36c-11.3 4.7-28.5 8-46.8 8-56.1-0.2-92.9-18.7-92.9-99z"
-                    p-id="10971" fill="#2c2c2c" stroke="#2c2c2c" stroke-width="20"></path>
-            </svg>
-        );
-    }
+  function CustomIcon() {
+    return (
+      <svg
+        t='1723135116886'
+        className='icon'
+        viewBox='0 0 1024 1024'
+        version='1.1'
+        xmlns='http://www.w3.org/2000/svg'
+        p-id='10969'
+        width='1em'
+        height='1em'
+      >
+        <path
+          d='M512 960C265 960 64 759 64 512S265 64 512 64s448 201 448 448-201 448-448 448z m0-882.6c-239.7 0-434.6 195-434.6 434.6s195 434.6 434.6 434.6 434.6-195 434.6-434.6S751.7 77.4 512 77.4z'
+          p-id='10970'
+          fill='#2c2c2c'
+          stroke='#2c2c2c'
+          stroke-width='60'
+        ></path>
+        <path
+          d='M197.7 512c0-78.3 31.6-98.8 87.2-98.8 56.2 0 87.2 20.5 87.2 98.8s-31 98.8-87.2 98.8c-55.7 0-87.2-20.5-87.2-98.8z m130.4 0c0-46.8-7.8-64.5-43.2-64.5-35.2 0-42.9 17.7-42.9 64.5 0 47.1 7.8 63.7 42.9 63.7 35.4 0 43.2-16.6 43.2-63.7zM409.7 415.9h42.1V608h-42.1V415.9zM653.9 512c0 74.2-37.1 96.1-93.6 96.1h-65.9V415.9h65.9c56.5 0 93.6 16.1 93.6 96.1z m-43.5 0c0-49.3-17.7-60.6-52.3-60.6h-21.6v120.7h21.6c35.4 0 52.3-13.3 52.3-60.1zM686.5 512c0-74.2 36.3-98.8 92.7-98.8 18.3 0 33.2 2.2 44.8 6.4v36.3c-11.9-4.2-26-6.6-42.1-6.6-34.6 0-49.8 15.5-49.8 62.6 0 50.1 15.2 62.6 49.3 62.6 15.8 0 30.2-2.2 44.8-7.5v36c-11.3 4.7-28.5 8-46.8 8-56.1-0.2-92.9-18.7-92.9-99z'
+          p-id='10971'
+          fill='#2c2c2c'
+          stroke='#2c2c2c'
+          stroke-width='20'
+        ></path>
+      </svg>
+    );
+  }
 
-    return <Icon svg={<CustomIcon />} />;
+  return <Icon svg={<CustomIcon />} />;
 };
 
-export default OIDCIcon;
+export default OIDCIcon;

+ 5 - 6
web/src/components/OperationSetting.js

@@ -11,7 +11,6 @@ import ModelSettingsVisualEditor from '../pages/Setting/Operation/ModelSettingsV
 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';
@@ -58,7 +57,7 @@ const OperationSetting = () => {
     DataExportInterval: 5,
     DefaultCollapseSidebar: false, // 默认折叠侧边栏
     RetryTimes: 0,
-    Chats: "[]",
+    Chats: '[]',
     DemoSiteEnabled: false,
     SelfUseModeEnabled: false,
     AutomaticDisableKeywords: '',
@@ -154,14 +153,14 @@ const OperationSetting = () => {
         </Card>
         {/* 合并模型倍率设置和可视化倍率设置 */}
         <Card style={{ marginTop: '10px' }}>
-          <Tabs type="line">
-            <Tabs.TabPane tab={t('模型倍率设置')} itemKey="model">
+          <Tabs type='line'>
+            <Tabs.TabPane tab={t('模型倍率设置')} itemKey='model'>
               <ModelRatioSettings options={inputs} refresh={onRefresh} />
             </Tabs.TabPane>
-            <Tabs.TabPane tab={t('可视化倍率设置')} itemKey="visual">
+            <Tabs.TabPane tab={t('可视化倍率设置')} itemKey='visual'>
               <ModelSettingsVisualEditor options={inputs} refresh={onRefresh} />
             </Tabs.TabPane>
-            <Tabs.TabPane tab={t('未设置倍率模型')} itemKey="unset_models">
+            <Tabs.TabPane tab={t('未设置倍率模型')} itemKey='unset_models'>
               <ModelRatioNotSetEditor options={inputs} refresh={onRefresh} />
             </Tabs.TabPane>
           </Tabs>

+ 162 - 115
web/src/components/OtherSetting.js

@@ -1,5 +1,14 @@
 import React, { useContext, useEffect, useRef, useState } from 'react';
-import { Banner, Button, Col, Form, Row, Modal, Space } from '@douyinfe/semi-ui';
+import {
+  Banner,
+  Button,
+  Col,
+  Form,
+  Row,
+  Modal,
+  Space,
+  Card,
+} from '@douyinfe/semi-ui';
 import { API, showError, showSuccess, timestamp2string } from '../helpers';
 import { marked } from 'marked';
 import { useTranslation } from 'react-i18next';
@@ -46,7 +55,7 @@ const OtherSetting = () => {
     HomePageContent: false,
     About: false,
     Footer: false,
-    CheckUpdate: false
+    CheckUpdate: false,
   });
   const handleInputChange = async (value, e) => {
     const name = e.target.id;
@@ -151,27 +160,30 @@ const OtherSetting = () => {
 
   const checkUpdate = async () => {
     try {
-      setLoadingInput((loadingInput) => ({ ...loadingInput, CheckUpdate: true }));
+      setLoadingInput((loadingInput) => ({
+        ...loadingInput,
+        CheckUpdate: true,
+      }));
       // Use a CORS proxy to avoid direct cross-origin requests to GitHub API
       // Option 1: Use a public CORS proxy service
       // const proxyUrl = 'https://cors-anywhere.herokuapp.com/';
       // const res = await API.get(
       //   `${proxyUrl}https://api.github.com/repos/Calcium-Ion/new-api/releases/latest`,
       // );
-      
+
       // Option 2: Use the JSON proxy approach which often works better with GitHub API
       const res = await fetch(
         'https://api.github.com/repos/Calcium-Ion/new-api/releases/latest',
         {
           headers: {
-            'Accept': 'application/json',
+            Accept: 'application/json',
             'Content-Type': 'application/json',
             // Adding User-Agent which is often required by GitHub API
-            'User-Agent': 'new-api-update-checker'
-          }
-        }
-      ).then(response => response.json());
-      
+            'User-Agent': 'new-api-update-checker',
+          },
+        },
+      ).then((response) => response.json());
+
       // Option 3: Use a local proxy endpoint
       // Create a cached version of the response to avoid frequent GitHub API calls
       // const res = await API.get('/api/status/github-latest-release');
@@ -190,7 +202,10 @@ const OtherSetting = () => {
       console.error('Failed to check for updates:', error);
       showError('检查更新失败,请稍后再试');
     } finally {
-      setLoadingInput((loadingInput) => ({ ...loadingInput, CheckUpdate: false }));
+      setLoadingInput((loadingInput) => ({
+        ...loadingInput,
+        CheckUpdate: false,
+      }));
     }
   };
   const getOptions = async () => {
@@ -217,7 +232,10 @@ const OtherSetting = () => {
 
   // Function to open GitHub release page
   const openGitHubRelease = () => {
-    window.open(`https://github.com/Calcium-Ion/new-api/releases/tag/${updateData.tag_name}`, '_blank');
+    window.open(
+      `https://github.com/Calcium-Ion/new-api/releases/tag/${updateData.tag_name}`,
+      '_blank',
+    );
   };
 
   const getStartTimeString = () => {
@@ -227,120 +245,149 @@ const OtherSetting = () => {
 
   return (
     <Row>
-      <Col span={24}>
+      <Col
+        span={24}
+        style={{
+          marginTop: '10px',
+          display: 'flex',
+          flexDirection: 'column',
+          gap: '10px',
+        }}
+      >
         {/* 版本信息 */}
-        <Form style={{ marginBottom: 15 }}>
-          <Form.Section text={t('系统信息')}>
-            <Row>
-              <Col span={16}>
-                <Space>
+        <Form>
+          <Card>
+            <Form.Section text={t('系统信息')}>
+              <Row>
+                <Col span={16}>
+                  <Space>
+                    <Text>
+                      {t('当前版本')}:
+                      {statusState?.status?.version || t('未知')}
+                    </Text>
+                    <Button
+                      type='primary'
+                      onClick={checkUpdate}
+                      loading={loadingInput['CheckUpdate']}
+                    >
+                      {t('检查更新')}
+                    </Button>
+                  </Space>
+                </Col>
+              </Row>
+              <Row>
+                <Col span={16}>
                   <Text>
-                    {t('当前版本')}:{statusState?.status?.version || t('未知')}
+                    {t('启动时间')}:{getStartTimeString()}
                   </Text>
-                  <Button type="primary" onClick={checkUpdate} loading={loadingInput['CheckUpdate']}>
-                    {t('检查更新')}
-                  </Button>
-                </Space>
-              </Col>
-            </Row>
-            <Row>
-              <Col span={16}>
-                <Text>{t('启动时间')}:{getStartTimeString()}</Text>
-              </Col>
-            </Row>
-          </Form.Section>
+                </Col>
+              </Row>
+            </Form.Section>
+          </Card>
         </Form>
         {/* 通用设置 */}
         <Form
           values={inputs}
           getFormApi={(formAPI) => (formAPISettingGeneral.current = formAPI)}
-          style={{ marginBottom: 15 }}
         >
-          <Form.Section text={t('通用设置')}>
-            <Form.TextArea
-              label={t('公告')}
-              placeholder={t('在此输入新的公告内容,支持 Markdown & HTML 代码')}
-              field={'Notice'}
-              onChange={handleInputChange}
-              style={{ fontFamily: 'JetBrains Mono, Consolas' }}
-              autosize={{ minRows: 6, maxRows: 12 }}
-            />
-            <Button onClick={submitNotice} loading={loadingInput['Notice']}>
-              {t('设置公告')}
-            </Button>
-          </Form.Section>
+          <Card>
+            <Form.Section text={t('通用设置')}>
+              <Form.TextArea
+                label={t('公告')}
+                placeholder={t(
+                  '在此输入新的公告内容,支持 Markdown & HTML 代码',
+                )}
+                field={'Notice'}
+                onChange={handleInputChange}
+                style={{ fontFamily: 'JetBrains Mono, Consolas' }}
+                autosize={{ minRows: 6, maxRows: 12 }}
+              />
+              <Button onClick={submitNotice} loading={loadingInput['Notice']}>
+                {t('设置公告')}
+              </Button>
+            </Form.Section>
+          </Card>
         </Form>
         {/* 个性化设置 */}
         <Form
           values={inputs}
           getFormApi={(formAPI) => (formAPIPersonalization.current = formAPI)}
-          style={{ marginBottom: 15 }}
         >
-          <Form.Section text={t('个性化设置')}>
-            <Form.Input
-              label={t('系统名称')}
-              placeholder={t('在此输入系统名称')}
-              field={'SystemName'}
-              onChange={handleInputChange}
-            />
-            <Button
-              onClick={submitSystemName}
-              loading={loadingInput['SystemName']}
-            >
-              {t('设置系统名称')}
-            </Button>
-            <Form.Input
-              label={t('Logo 图片地址')}
-              placeholder={t('在此输入 Logo 图片地址')}
-              field={'Logo'}
-              onChange={handleInputChange}
-            />
-            <Button onClick={submitLogo} loading={loadingInput['Logo']}>
-              {t('设置 Logo')}
-            </Button>
-            <Form.TextArea
-              label={t('首页内容')}
-              placeholder={t('在此输入首页内容,支持 Markdown & HTML 代码,设置后首页的状态信息将不再显示。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为首页')}
-              field={'HomePageContent'}
-              onChange={handleInputChange}
-              style={{ fontFamily: 'JetBrains Mono, Consolas' }}
-              autosize={{ minRows: 6, maxRows: 12 }}
-            />
-            <Button
-              onClick={() => submitOption('HomePageContent')}
-              loading={loadingInput['HomePageContent']}
-            >
-              {t('设置首页内容')}
-            </Button>
-            <Form.TextArea
-              label={t('关于')}
-              placeholder={t('在此输入新的关于内容,支持 Markdown & HTML 代码。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为关于页面')}
-              field={'About'}
-              onChange={handleInputChange}
-              style={{ fontFamily: 'JetBrains Mono, Consolas' }}
-              autosize={{ minRows: 6, maxRows: 12 }}
-            />
-            <Button onClick={submitAbout} loading={loadingInput['About']}>
-              {t('设置关于')}
-            </Button>
-            {/*  */}
-            <Banner
-              fullMode={false}
-              type='info'
-              description={t('移除 One API 的版权标识必须首先获得授权,项目维护需要花费大量精力,如果本项目对你有意义,请主动支持本项目')}
-              closeIcon={null}
-              style={{ marginTop: 15 }}
-            />
-            <Form.Input
-              label={t('页脚')}
-              placeholder={t('在此输入新的页脚,留空则使用默认页脚,支持 HTML 代码')}
-              field={'Footer'}
-              onChange={handleInputChange}
-            />
-            <Button onClick={submitFooter} loading={loadingInput['Footer']}>
-              {t('设置页脚')}
-            </Button>
-          </Form.Section>
+          <Card>
+            <Form.Section text={t('个性化设置')}>
+              <Form.Input
+                label={t('系统名称')}
+                placeholder={t('在此输入系统名称')}
+                field={'SystemName'}
+                onChange={handleInputChange}
+              />
+              <Button
+                onClick={submitSystemName}
+                loading={loadingInput['SystemName']}
+              >
+                {t('设置系统名称')}
+              </Button>
+              <Form.Input
+                label={t('Logo 图片地址')}
+                placeholder={t('在此输入 Logo 图片地址')}
+                field={'Logo'}
+                onChange={handleInputChange}
+              />
+              <Button onClick={submitLogo} loading={loadingInput['Logo']}>
+                {t('设置 Logo')}
+              </Button>
+              <Form.TextArea
+                label={t('首页内容')}
+                placeholder={t(
+                  '在此输入首页内容,支持 Markdown & HTML 代码,设置后首页的状态信息将不再显示。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为首页',
+                )}
+                field={'HomePageContent'}
+                onChange={handleInputChange}
+                style={{ fontFamily: 'JetBrains Mono, Consolas' }}
+                autosize={{ minRows: 6, maxRows: 12 }}
+              />
+              <Button
+                onClick={() => submitOption('HomePageContent')}
+                loading={loadingInput['HomePageContent']}
+              >
+                {t('设置首页内容')}
+              </Button>
+              <Form.TextArea
+                label={t('关于')}
+                placeholder={t(
+                  '在此输入新的关于内容,支持 Markdown & HTML 代码。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为关于页面',
+                )}
+                field={'About'}
+                onChange={handleInputChange}
+                style={{ fontFamily: 'JetBrains Mono, Consolas' }}
+                autosize={{ minRows: 6, maxRows: 12 }}
+              />
+              <Button onClick={submitAbout} loading={loadingInput['About']}>
+                {t('设置关于')}
+              </Button>
+              {/*  */}
+              <Banner
+                fullMode={false}
+                type='info'
+                description={t(
+                  '移除 One API 的版权标识必须首先获得授权,项目维护需要花费大量精力,如果本项目对你有意义,请主动支持本项目',
+                )}
+                closeIcon={null}
+                style={{ marginTop: 15 }}
+              />
+              <Form.Input
+                label={t('页脚')}
+                placeholder={t(
+                  '在此输入新的页脚,留空则使用默认页脚,支持 HTML 代码',
+                )}
+                field={'Footer'}
+                onChange={handleInputChange}
+              />
+              <Button onClick={submitFooter} loading={loadingInput['Footer']}>
+                {t('设置页脚')}
+              </Button>
+            </Form.Section>
+          </Card>
         </Form>
       </Col>
       <Modal
@@ -348,16 +395,16 @@ const OtherSetting = () => {
         visible={showUpdateModal}
         onCancel={() => setShowUpdateModal(false)}
         footer={[
-          <Button 
-            key="details" 
-            type="primary" 
+          <Button
+            key='details'
+            type='primary'
             onClick={() => {
               setShowUpdateModal(false);
               openGitHubRelease();
             }}
           >
             {t('详情')}
-          </Button>
+          </Button>,
         ]}
       >
         <div dangerouslySetInnerHTML={{ __html: updateData.content }}></div>

+ 71 - 53
web/src/components/PageLayout.js

@@ -13,7 +13,6 @@ import { UserContext } from '../context/User/index.js';
 import { StatusContext } from '../context/Status/index.js';
 const { Sider, Content, Header, Footer } = Layout;
 
-
 const PageLayout = () => {
   const [userState, userDispatch] = useContext(UserContext);
   const [statusState, statusDispatch] = useContext(StatusContext);
@@ -62,85 +61,104 @@ const PageLayout = () => {
     if (savedLang) {
       i18n.changeLanguage(savedLang);
     }
-    
+
     // 默认显示侧边栏
     styleDispatch({ type: 'SET_SIDER', payload: true });
   }, [i18n]);
 
   // 获取侧边栏折叠状态
-  const isSidebarCollapsed = localStorage.getItem('default_collapse_sidebar') === 'true';
+  const isSidebarCollapsed =
+    localStorage.getItem('default_collapse_sidebar') === 'true';
 
   return (
-    <Layout style={{ 
-      height: '100vh', 
-      display: 'flex', 
-      flexDirection: 'column',
-      overflow: styleState.isMobile ? 'visible' : 'hidden'
-    }}>
-      <Header style={{ 
-        padding: 0, 
-        height: 'auto', 
-        lineHeight: 'normal', 
-        position: styleState.isMobile ? 'sticky' : 'fixed',
-        width: '100%', 
-        top: 0, 
-        zIndex: 100,
-        boxShadow: '0 1px 6px rgba(0, 0, 0, 0.08)'
-      }}>
+    <Layout
+      style={{
+        height: '100vh',
+        display: 'flex',
+        flexDirection: 'column',
+        overflow: styleState.isMobile ? 'visible' : 'hidden',
+      }}
+    >
+      <Header
+        style={{
+          padding: 0,
+          height: 'auto',
+          lineHeight: 'normal',
+          position: styleState.isMobile ? 'sticky' : 'fixed',
+          width: '100%',
+          top: 0,
+          zIndex: 100,
+          boxShadow: '0 1px 6px rgba(0, 0, 0, 0.08)',
+        }}
+      >
         <HeaderBar />
       </Header>
-      <Layout style={{ 
-        marginTop: styleState.isMobile ? '0' : '56px',
-        height: styleState.isMobile ? 'auto' : 'calc(100vh - 56px)',
-        overflow: styleState.isMobile ? 'visible' : 'auto',
-        display: 'flex',
-        flexDirection: 'column'
-      }}>
+      <Layout
+        style={{
+          marginTop: styleState.isMobile ? '0' : '56px',
+          height: styleState.isMobile ? 'auto' : 'calc(100vh - 56px)',
+          overflow: styleState.isMobile ? 'visible' : 'auto',
+          display: 'flex',
+          flexDirection: 'column',
+        }}
+      >
         {styleState.showSider && (
-          <Sider style={{
-            position: 'fixed',
-            left: 0,
-            top: '56px',
-            zIndex: 99,
-            background: 'var(--semi-color-bg-1)',
-            boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
-            border: 'none',
-            paddingRight: '0',
-            height: 'calc(100vh - 56px)',
-          }}>
+          <Sider
+            style={{
+              position: 'fixed',
+              left: 0,
+              top: '56px',
+              zIndex: 99,
+              background: 'var(--semi-color-bg-1)',
+              boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
+              border: 'none',
+              paddingRight: '0',
+              height: 'calc(100vh - 56px)',
+            }}
+          >
             <SiderBar />
           </Sider>
         )}
-        <Layout style={{ 
-          marginLeft: styleState.isMobile ? '0' : (styleState.showSider ? (styleState.siderCollapsed ? '60px' : '200px') : '0'),
-          transition: 'margin-left 0.3s ease',
-          flex: '1 1 auto',
-          display: 'flex',
-          flexDirection: 'column'
-        }}>
+        <Layout
+          style={{
+            marginLeft: styleState.isMobile
+              ? '0'
+              : styleState.showSider
+                ? styleState.siderCollapsed
+                  ? '60px'
+                  : '200px'
+                : '0',
+            transition: 'margin-left 0.3s ease',
+            flex: '1 1 auto',
+            display: 'flex',
+            flexDirection: 'column',
+          }}
+        >
           <Content
-            style={{ 
+            style={{
               flex: '1 0 auto',
               overflowY: styleState.isMobile ? 'visible' : 'auto',
               WebkitOverflowScrolling: 'touch',
-              padding: styleState.shouldInnerPadding? '24px': '0',
+              padding: styleState.shouldInnerPadding ? '24px' : '0',
               position: 'relative',
               marginTop: styleState.isMobile ? '2px' : '0',
             }}
           >
             <App />
           </Content>
-          <Layout.Footer style={{ 
-            flex: '0 0 auto',
-            width: '100%'
-          }}>
+          <Layout.Footer
+            style={{
+              flex: '0 0 auto',
+              width: '100%',
+            }}
+          >
             <FooterBar />
           </Layout.Footer>
         </Layout>
       </Layout>
       <ToastContainer />
     </Layout>
-  )
-}
+  );
+};
 
-export default PageLayout;
+export default PageLayout;

+ 192 - 104
web/src/components/PersonalSetting.js

@@ -6,11 +6,15 @@ import {
   isRoot,
   showError,
   showInfo,
-  showSuccess
+  showSuccess,
 } from '../helpers';
 import Turnstile from 'react-turnstile';
 import { UserContext } from '../context/User';
-import { onGitHubOAuthClicked, onOIDCClicked, onLinuxDOOAuthClicked } from './utils';
+import {
+  onGitHubOAuthClicked,
+  onOIDCClicked,
+  onLinuxDOOAuthClicked,
+} from './utils';
 import {
   Avatar,
   Banner,
@@ -32,13 +36,13 @@ import {
   AutoComplete,
   Checkbox,
   Tabs,
-  TabPane
+  TabPane,
 } from '@douyinfe/semi-ui';
 import {
   getQuotaPerUnit,
   renderQuota,
   renderQuotaWithPrompt,
-  stringToColor
+  stringToColor,
 } from '../helpers/render';
 import TelegramLoginButton from 'react-telegram-login';
 import { useTranslation } from 'react-i18next';
@@ -54,7 +58,7 @@ const PersonalSetting = () => {
     email: '',
     self_account_deletion_confirmation: '',
     set_new_password: '',
-    set_new_password_confirmation: ''
+    set_new_password_confirmation: '',
   });
   const [status, setStatus] = useState({});
   const [showChangePasswordModal, setShowChangePasswordModal] = useState(false);
@@ -77,14 +81,14 @@ const PersonalSetting = () => {
     const savedState = localStorage.getItem('modelsExpanded');
     return savedState ? JSON.parse(savedState) : false;
   });
-  const MODELS_DISPLAY_COUNT = 10;  // 默认显示的模型数量
+  const MODELS_DISPLAY_COUNT = 10; // 默认显示的模型数量
   const [notificationSettings, setNotificationSettings] = useState({
     warningType: 'email',
     warningThreshold: 100000,
     webhookUrl: '',
     webhookSecret: '',
     notificationEmail: '',
-    acceptUnsetModelRatioModel: false
+    acceptUnsetModelRatioModel: false,
   });
   const [showWebhookDocs, setShowWebhookDocs] = useState(false);
 
@@ -128,7 +132,8 @@ const PersonalSetting = () => {
         webhookUrl: settings.webhook_url || '',
         webhookSecret: settings.webhook_secret || '',
         notificationEmail: settings.notification_email || '',
-        acceptUnsetModelRatioModel: settings.accept_unset_model_ratio_model || false
+        acceptUnsetModelRatioModel:
+          settings.accept_unset_model_ratio_model || false,
       });
     }
   }, [userState?.user?.setting]);
@@ -222,7 +227,7 @@ const PersonalSetting = () => {
   const bindWeChat = async () => {
     if (inputs.wechat_verification_code === '') return;
     const res = await API.get(
-      `/api/oauth/wechat/bind?code=${inputs.wechat_verification_code}`
+      `/api/oauth/wechat/bind?code=${inputs.wechat_verification_code}`,
     );
     const { success, message } = res.data;
     if (success) {
@@ -239,7 +244,7 @@ const PersonalSetting = () => {
       return;
     }
     const res = await API.put(`/api/user/self`, {
-      password: inputs.set_new_password
+      password: inputs.set_new_password,
     });
     const { success, message } = res.data;
     if (success) {
@@ -257,7 +262,7 @@ const PersonalSetting = () => {
       return;
     }
     const res = await API.post(`/api/user/aff_transfer`, {
-      quota: transferAmount
+      quota: transferAmount,
     });
     const { success, message } = res.data;
     if (success) {
@@ -281,7 +286,7 @@ const PersonalSetting = () => {
     }
     setLoading(true);
     const res = await API.get(
-      `/api/verification?email=${inputs.email}&turnstile=${turnstileToken}`
+      `/api/verification?email=${inputs.email}&turnstile=${turnstileToken}`,
     );
     const { success, message } = res.data;
     if (success) {
@@ -299,7 +304,7 @@ const PersonalSetting = () => {
     }
     setLoading(true);
     const res = await API.get(
-      `/api/oauth/email/bind?email=${inputs.email}&code=${inputs.email_verification_code}`
+      `/api/oauth/email/bind?email=${inputs.email}&code=${inputs.email_verification_code}`,
     );
     const { success, message } = res.data;
     if (success) {
@@ -334,9 +339,9 @@ const PersonalSetting = () => {
   };
 
   const handleNotificationSettingChange = (type, value) => {
-    setNotificationSettings(prev => ({
+    setNotificationSettings((prev) => ({
       ...prev,
-      [type]: value.target ? value.target.value : value  // 处理 Radio 事件对象
+      [type]: value.target ? value.target.value : value, // 处理 Radio 事件对象
     }));
   };
 
@@ -344,11 +349,14 @@ const PersonalSetting = () => {
     try {
       const res = await API.put('/api/user/setting', {
         notify_type: notificationSettings.warningType,
-        quota_warning_threshold: parseFloat(notificationSettings.warningThreshold),
+        quota_warning_threshold: parseFloat(
+          notificationSettings.warningThreshold,
+        ),
         webhook_url: notificationSettings.webhookUrl,
         webhook_secret: notificationSettings.webhookSecret,
         notification_email: notificationSettings.notificationEmail,
-        accept_unset_model_ratio_model: notificationSettings.acceptUnsetModelRatioModel
+        accept_unset_model_ratio_model:
+          notificationSettings.acceptUnsetModelRatioModel,
       });
 
       if (res.data.success) {
@@ -363,7 +371,6 @@ const PersonalSetting = () => {
   };
 
   return (
-
     <div>
       <Layout>
         <Layout.Content>
@@ -377,7 +384,10 @@ const PersonalSetting = () => {
             centered={true}
           >
             <div style={{ marginTop: 20 }}>
-              <Typography.Text>{t('可用额度')}{renderQuotaWithPrompt(userState?.user?.aff_quota)}</Typography.Text>
+              <Typography.Text>
+                {t('可用额度')}
+                {renderQuotaWithPrompt(userState?.user?.aff_quota)}
+              </Typography.Text>
               <Input
                 style={{ marginTop: 5 }}
                 value={userState?.user?.aff_quota}
@@ -386,7 +396,9 @@ const PersonalSetting = () => {
             </div>
             <div style={{ marginTop: 20 }}>
               <Typography.Text>
-                {t('划转额度')}{renderQuotaWithPrompt(transferAmount)} {t('最低') + renderQuota(getQuotaPerUnit())}
+                {t('划转额度')}
+                {renderQuotaWithPrompt(transferAmount)}{' '}
+                {t('最低') + renderQuota(getQuotaPerUnit())}
               </Typography.Text>
               <div>
                 <InputNumber
@@ -405,7 +417,7 @@ const PersonalSetting = () => {
                 <Card.Meta
                   avatar={
                     <Avatar
-                      size="default"
+                      size='default'
                       color={stringToColor(getUsername())}
                       style={{ marginRight: 4 }}
                     >
@@ -416,25 +428,29 @@ const PersonalSetting = () => {
                   title={<Typography.Text>{getUsername()}</Typography.Text>}
                   description={
                     isRoot() ? (
-                      <Tag color="red">{t('管理员')}</Tag>
+                      <Tag color='red'>{t('管理员')}</Tag>
                     ) : (
-                      <Tag color="blue">{t('普通用户')}</Tag>
+                      <Tag color='blue'>{t('普通用户')}</Tag>
                     )
                   }
                 ></Card.Meta>
               }
               headerExtraContent={
                 <>
-                  <Space vertical align="start">
-                    <Tag color="green">{'ID: ' + userState?.user?.id}</Tag>
-                    <Tag color="blue">{userState?.user?.group}</Tag>
+                  <Space vertical align='start'>
+                    <Tag color='green'>{'ID: ' + userState?.user?.id}</Tag>
+                    <Tag color='blue'>{userState?.user?.group}</Tag>
                   </Space>
                 </>
               }
               footer={
                 <>
-                  <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
-                    <Typography.Title heading={6}>{t('可用模型')}</Typography.Title>
+                  <div
+                    style={{ display: 'flex', alignItems: 'center', gap: 8 }}
+                  >
+                    <Typography.Title heading={6}>
+                      {t('可用模型')}
+                    </Typography.Title>
                   </div>
                   <div style={{ marginTop: 10 }}>
                     {models.length <= MODELS_DISPLAY_COUNT ? (
@@ -442,7 +458,7 @@ const PersonalSetting = () => {
                         {models.map((model) => (
                           <Tag
                             key={model}
-                            color="cyan"
+                            color='cyan'
                             onClick={() => {
                               copyText(model);
                             }}
@@ -458,7 +474,7 @@ const PersonalSetting = () => {
                             {models.map((model) => (
                               <Tag
                                 key={model}
-                                color="cyan"
+                                color='cyan'
                                 onClick={() => {
                                   copyText(model);
                                 }}
@@ -467,8 +483,8 @@ const PersonalSetting = () => {
                               </Tag>
                             ))}
                             <Tag
-                              color="blue"
-                              type="light"
+                              color='blue'
+                              type='light'
                               style={{ cursor: 'pointer' }}
                               onClick={() => setIsModelsExpanded(false)}
                             >
@@ -478,24 +494,27 @@ const PersonalSetting = () => {
                         </Collapsible>
                         {!isModelsExpanded && (
                           <Space wrap>
-                            {models.slice(0, MODELS_DISPLAY_COUNT).map((model) => (
-                              <Tag
-                                key={model}
-                                color="cyan"
-                                onClick={() => {
-                                  copyText(model);
-                                }}
-                              >
-                                {model}
-                              </Tag>
-                            ))}
+                            {models
+                              .slice(0, MODELS_DISPLAY_COUNT)
+                              .map((model) => (
+                                <Tag
+                                  key={model}
+                                  color='cyan'
+                                  onClick={() => {
+                                    copyText(model);
+                                  }}
+                                >
+                                  {model}
+                                </Tag>
+                              ))}
                             <Tag
-                              color="blue"
-                              type="light"
+                              color='blue'
+                              type='light'
                               style={{ cursor: 'pointer' }}
                               onClick={() => setIsModelsExpanded(true)}
                             >
-                              {t('更多')} {models.length - MODELS_DISPLAY_COUNT} {t('个模型')}
+                              {t('更多')} {models.length - MODELS_DISPLAY_COUNT}{' '}
+                              {t('个模型')}
                             </Tag>
                           </Space>
                         )}
@@ -503,7 +522,6 @@ const PersonalSetting = () => {
                     )}
                   </div>
                 </>
-
               }
             >
               <Descriptions row>
@@ -536,9 +554,9 @@ const PersonalSetting = () => {
               <div style={{ marginTop: 10 }}>
                 <Descriptions row>
                   <Descriptions.Item itemKey={t('待使用收益')}>
-                                        <span style={{ color: 'rgba(var(--semi-red-5), 1)' }}>
-                                            {renderQuota(userState?.user?.aff_quota)}
-                                        </span>
+                    <span style={{ color: 'rgba(var(--semi-red-5), 1)' }}>
+                      {renderQuota(userState?.user?.aff_quota)}
+                    </span>
                     <Button
                       type={'secondary'}
                       onClick={() => setOpenTransfer(true)}
@@ -589,7 +607,9 @@ const PersonalSetting = () => {
               </div>
               <div style={{ marginTop: 10 }}>
                 <Typography.Text strong>{t('微信')}</Typography.Text>
-                <div style={{ display: 'flex', justifyContent: 'space-between' }}>
+                <div
+                  style={{ display: 'flex', justifyContent: 'space-between' }}
+                >
                   <div>
                     <Input
                       value={
@@ -664,7 +684,10 @@ const PersonalSetting = () => {
                   <div>
                     <Button
                       onClick={() => {
-                        onOIDCClicked(status.oidc_authorization_endpoint, status.oidc_client_id);
+                        onOIDCClicked(
+                          status.oidc_authorization_endpoint,
+                          status.oidc_client_id,
+                        );
                       }}
                       disabled={
                         (userState.user && userState.user.oidc_id !== '') ||
@@ -697,7 +720,7 @@ const PersonalSetting = () => {
                         <Button disabled={true}>{t('已绑定')}</Button>
                       ) : (
                         <TelegramLoginButton
-                          dataAuthUrl="/api/oauth/telegram/bind"
+                          dataAuthUrl='/api/oauth/telegram/bind'
                           botName={status.telegram_bot_name}
                         />
                       )
@@ -779,14 +802,14 @@ const PersonalSetting = () => {
                     </p>
                   </div>
                   <Input
-                    placeholder="验证码"
-                    name="wechat_verification_code"
+                    placeholder='验证码'
+                    name='wechat_verification_code'
                     value={inputs.wechat_verification_code}
                     onChange={(v) =>
                       handleInputChange('wechat_verification_code', v)
                     }
                   />
-                  <Button color="" fluid size="large" onClick={bindWeChat}>
+                  <Button color='' fluid size='large' onClick={bindWeChat}>
                     {t('绑定')}
                   </Button>
                 </Modal>
@@ -800,38 +823,62 @@ const PersonalSetting = () => {
                     <div style={{ marginTop: 10 }}>
                       <RadioGroup
                         value={notificationSettings.warningType}
-                        onChange={value => handleNotificationSettingChange('warningType', value)}
+                        onChange={(value) =>
+                          handleNotificationSettingChange('warningType', value)
+                        }
                       >
-                        <Radio value="email">{t('邮件通知')}</Radio>
-                        <Radio value="webhook">{t('Webhook通知')}</Radio>
+                        <Radio value='email'>{t('邮件通知')}</Radio>
+                        <Radio value='webhook'>{t('Webhook通知')}</Radio>
                       </RadioGroup>
                     </div>
                   </div>
                   {notificationSettings.warningType === 'webhook' && (
                     <>
                       <div style={{ marginTop: 20 }}>
-                        <Typography.Text strong>{t('Webhook地址')}</Typography.Text>
+                        <Typography.Text strong>
+                          {t('Webhook地址')}
+                        </Typography.Text>
                         <div style={{ marginTop: 10 }}>
                           <Input
                             value={notificationSettings.webhookUrl}
-                            onChange={val => handleNotificationSettingChange('webhookUrl', val)}
-                            placeholder={t('请输入Webhook地址,例如: https://example.com/webhook')}
+                            onChange={(val) =>
+                              handleNotificationSettingChange('webhookUrl', val)
+                            }
+                            placeholder={t(
+                              '请输入Webhook地址,例如: https://example.com/webhook',
+                            )}
                           />
-                          <Typography.Text type="secondary" style={{ marginTop: 8, display: 'block' }}>
-                            {t('只支持https,系统将以 POST 方式发送通知,请确保地址可以接收 POST 请求')}
+                          <Typography.Text
+                            type='secondary'
+                            style={{ marginTop: 8, display: 'block' }}
+                          >
+                            {t(
+                              '只支持https,系统将以 POST 方式发送通知,请确保地址可以接收 POST 请求',
+                            )}
                           </Typography.Text>
-                          <Typography.Text type="secondary" style={{ marginTop: 8, display: 'block' }}>
-                            <div style={{ cursor: 'pointer' }} onClick={() => setShowWebhookDocs(!showWebhookDocs)}>
-                              {t('Webhook请求结构')} {showWebhookDocs ? '▼' : '▶'}
+                          <Typography.Text
+                            type='secondary'
+                            style={{ marginTop: 8, display: 'block' }}
+                          >
+                            <div
+                              style={{ cursor: 'pointer' }}
+                              onClick={() =>
+                                setShowWebhookDocs(!showWebhookDocs)
+                              }
+                            >
+                              {t('Webhook请求结构')}{' '}
+                              {showWebhookDocs ? '▼' : '▶'}
                             </div>
                             <Collapsible isOpen={showWebhookDocs}>
-                            <pre style={{
-                              marginTop: 4,
-                              background: 'var(--semi-color-fill-0)',
-                              padding: 8,
-                              borderRadius: 4
-                            }}>
-{`{
+                              <pre
+                                style={{
+                                  marginTop: 4,
+                                  background: 'var(--semi-color-fill-0)',
+                                  padding: 8,
+                                  borderRadius: 4,
+                                }}
+                              >
+                                {`{
     "type": "quota_exceed",      // 通知类型
     "title": "标题",             // 通知标题
     "content": "通知内容",       // 通知内容,支持 {{value}} 变量占位符
@@ -847,23 +894,38 @@ const PersonalSetting = () => {
     "values": ["$0.99"],
     "timestamp": 1739950503
 }`}
-                            </pre>
+                              </pre>
                             </Collapsible>
                           </Typography.Text>
                         </div>
                       </div>
                       <div style={{ marginTop: 20 }}>
-                        <Typography.Text strong>{t('接口凭证(可选)')}</Typography.Text>
+                        <Typography.Text strong>
+                          {t('接口凭证(可选)')}
+                        </Typography.Text>
                         <div style={{ marginTop: 10 }}>
                           <Input
                             value={notificationSettings.webhookSecret}
-                            onChange={val => handleNotificationSettingChange('webhookSecret', val)}
+                            onChange={(val) =>
+                              handleNotificationSettingChange(
+                                'webhookSecret',
+                                val,
+                              )
+                            }
                             placeholder={t('请输入密钥')}
                           />
-                          <Typography.Text type="secondary" style={{ marginTop: 8, display: 'block' }}>
-                            {t('密钥将以 Bearer 方式添加到请求头中,用于验证webhook请求的合法性')}
+                          <Typography.Text
+                            type='secondary'
+                            style={{ marginTop: 8, display: 'block' }}
+                          >
+                            {t(
+                              '密钥将以 Bearer 方式添加到请求头中,用于验证webhook请求的合法性',
+                            )}
                           </Typography.Text>
-                          <Typography.Text type="secondary" style={{ marginTop: 4, display: 'block' }}>
+                          <Typography.Text
+                            type='secondary'
+                            style={{ marginTop: 4, display: 'block' }}
+                          >
                             {t('Authorization: Bearer your-secret-key')}
                           </Typography.Text>
                         </div>
@@ -876,34 +938,58 @@ const PersonalSetting = () => {
                       <div style={{ marginTop: 10 }}>
                         <Input
                           value={notificationSettings.notificationEmail}
-                          onChange={val => handleNotificationSettingChange('notificationEmail', val)}
+                          onChange={(val) =>
+                            handleNotificationSettingChange(
+                              'notificationEmail',
+                              val,
+                            )
+                          }
                           placeholder={t('留空则使用账号绑定的邮箱')}
                         />
-                        <Typography.Text type="secondary" style={{ marginTop: 8, display: 'block' }}>
-                          {t('设置用于接收额度预警的邮箱地址,不填则使用账号绑定的邮箱')}
+                        <Typography.Text
+                          type='secondary'
+                          style={{ marginTop: 8, display: 'block' }}
+                        >
+                          {t(
+                            '设置用于接收额度预警的邮箱地址,不填则使用账号绑定的邮箱',
+                          )}
                         </Typography.Text>
                       </div>
                     </div>
                   )}
                   <div style={{ marginTop: 20 }}>
-                    <Typography.Text
-                      strong>{t('额度预警阈值')} {renderQuotaWithPrompt(notificationSettings.warningThreshold)}</Typography.Text>
+                    <Typography.Text strong>
+                      {t('额度预警阈值')}{' '}
+                      {renderQuotaWithPrompt(
+                        notificationSettings.warningThreshold,
+                      )}
+                    </Typography.Text>
                     <div style={{ marginTop: 10 }}>
                       <AutoComplete
                         value={notificationSettings.warningThreshold}
-                        onChange={val => handleNotificationSettingChange('warningThreshold', val)}
+                        onChange={(val) =>
+                          handleNotificationSettingChange(
+                            'warningThreshold',
+                            val,
+                          )
+                        }
                         style={{ width: 200 }}
                         placeholder={t('请输入预警额度')}
                         data={[
                           { value: 100000, label: '0.2$' },
                           { value: 500000, label: '1$' },
                           { value: 1000000, label: '5$' },
-                          { value: 5000000, label: '10$' }
+                          { value: 5000000, label: '10$' },
                         ]}
                       />
                     </div>
-                    <Typography.Text type="secondary" style={{ marginTop: 10, display: 'block' }}>
-                      {t('当剩余额度低于此数值时,系统将通过选择的方式发送通知')}
+                    <Typography.Text
+                      type='secondary'
+                      style={{ marginTop: 10, display: 'block' }}
+                    >
+                      {t(
+                        '当剩余额度低于此数值时,系统将通过选择的方式发送通知',
+                      )}
                     </Typography.Text>
                   </div>
                 </TabPane>
@@ -923,10 +1009,10 @@ const PersonalSetting = () => {
                     </div>
                   </div>
                 </TabPane>
-                
+
               </Tabs>
               <div style={{ marginTop: 20 }}>
-                <Button type="primary" onClick={saveNotificationSettings}>
+                <Button type='primary' onClick={saveNotificationSettings}>
                   {t('保存设置')}
                 </Button>
               </div>
@@ -939,20 +1025,22 @@ const PersonalSetting = () => {
               centered={true}
               maskClosable={false}
             >
-              <Typography.Title heading={6}>{t('绑定邮箱地址')}</Typography.Title>
+              <Typography.Title heading={6}>
+                {t('绑定邮箱地址')}
+              </Typography.Title>
               <div
                 style={{
                   marginTop: 20,
                   display: 'flex',
-                  justifyContent: 'space-between'
+                  justifyContent: 'space-between',
                 }}
               >
                 <Input
                   fluid
-                  placeholder="输入邮箱地址"
+                  placeholder='输入邮箱地址'
                   onChange={(value) => handleInputChange('email', value)}
-                  name="email"
-                  type="email"
+                  name='email'
+                  type='email'
                 />
                 <Button
                   onClick={sendVerificationCode}
@@ -964,8 +1052,8 @@ const PersonalSetting = () => {
               <div style={{ marginTop: 10 }}>
                 <Input
                   fluid
-                  placeholder="验证码"
-                  name="email_verification_code"
+                  placeholder='验证码'
+                  name='email_verification_code'
                   value={inputs.email_verification_code}
                   onChange={(value) =>
                     handleInputChange('email_verification_code', value)
@@ -992,20 +1080,20 @@ const PersonalSetting = () => {
             >
               <div style={{ marginTop: 20 }}>
                 <Banner
-                  type="danger"
-                  description="您正在删除自己的帐户,将清空所有数据且不可恢复"
+                  type='danger'
+                  description='您正在删除自己的帐户,将清空所有数据且不可恢复'
                   closeIcon={null}
                 />
               </div>
               <div style={{ marginTop: 20 }}>
                 <Input
                   placeholder={`输入你的账户名 ${userState?.user?.username} 以确认删除`}
-                  name="self_account_deletion_confirmation"
+                  name='self_account_deletion_confirmation'
                   value={inputs.self_account_deletion_confirmation}
                   onChange={(value) =>
                     handleInputChange(
                       'self_account_deletion_confirmation',
-                      value
+                      value,
                     )
                   }
                 />
@@ -1030,7 +1118,7 @@ const PersonalSetting = () => {
             >
               <div style={{ marginTop: 20 }}>
                 <Input
-                  name="set_new_password"
+                  name='set_new_password'
                   placeholder={t('新密码')}
                   value={inputs.set_new_password}
                   onChange={(value) =>
@@ -1039,7 +1127,7 @@ const PersonalSetting = () => {
                 />
                 <Input
                   style={{ marginTop: 20 }}
-                  name="set_new_password_confirmation"
+                  name='set_new_password_confirmation'
                   placeholder={t('确认新密码')}
                   value={inputs.set_new_password_confirmation}
                   onChange={(value) =>

+ 1 - 4
web/src/components/RateLimitSetting.js

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

+ 50 - 42
web/src/components/RedemptionsTable.js

@@ -10,7 +10,8 @@ import {
 import { ITEMS_PER_PAGE } from '../constants';
 import { renderQuota } from '../helpers/render';
 import {
-  Button, Divider,
+  Button,
+  Divider,
   Form,
   Modal,
   Popconfirm,
@@ -193,15 +194,17 @@ const RedemptionsTable = () => {
   };
 
   const loadRedemptions = async (startIdx, pageSize) => {
-    const res = await API.get(`/api/redemption/?p=${startIdx}&page_size=${pageSize}`);
+    const res = await API.get(
+      `/api/redemption/?p=${startIdx}&page_size=${pageSize}`,
+    );
     const { success, message, data } = res.data;
     if (success) {
-        const newPageData = data.items;
-        setActivePage(data.page);
-        setTokenCount(data.total);
-        setRedemptionFormat(newPageData);
+      const newPageData = data.items;
+      setActivePage(data.page);
+      setTokenCount(data.total);
+      setRedemptionFormat(newPageData);
     } else {
-        showError(message);
+      showError(message);
     }
     setLoading(false);
   };
@@ -282,19 +285,21 @@ const RedemptionsTable = () => {
 
   const searchRedemptions = async (keyword, page, pageSize) => {
     if (searchKeyword === '') {
-        await loadRedemptions(page, pageSize);
-        return;
+      await loadRedemptions(page, pageSize);
+      return;
     }
     setSearching(true);
-    const res = await API.get(`/api/redemption/search?keyword=${keyword}&p=${page}&page_size=${pageSize}`);
+    const res = await API.get(
+      `/api/redemption/search?keyword=${keyword}&p=${page}&page_size=${pageSize}`,
+    );
     const { success, message, data } = res.data;
     if (success) {
-        const newPageData = data.items;
-        setActivePage(data.page);
-        setTokenCount(data.total);
-        setRedemptionFormat(newPageData);
+      const newPageData = data.items;
+      setActivePage(data.page);
+      setTokenCount(data.total);
+      setRedemptionFormat(newPageData);
     } else {
-        showError(message);
+      showError(message);
     }
     setSearching(false);
   };
@@ -355,9 +360,11 @@ const RedemptionsTable = () => {
         visiable={showEdit}
         handleClose={closeEdit}
       ></EditRedemption>
-      <Form onSubmit={()=> {
-        searchRedemptions(searchKeyword, activePage, pageSize).then();
-      }}>
+      <Form
+        onSubmit={() => {
+          searchRedemptions(searchKeyword, activePage, pageSize).then();
+        }}
+      >
         <Form.Input
           label={t('搜索关键字')}
           field='keyword'
@@ -369,35 +376,36 @@ const RedemptionsTable = () => {
           onChange={handleKeywordChange}
         />
       </Form>
-      <Divider style={{margin:'5px 0 15px 0'}}/>
+      <Divider style={{ margin: '5px 0 15px 0' }} />
       <div>
         <Button
-            theme='light'
-            type='primary'
-            style={{ marginRight: 8 }}
-            onClick={() => {
-              setEditingRedemption({
-                id: undefined,
-              });
-              setShowEdit(true);
-            }}
+          theme='light'
+          type='primary'
+          style={{ marginRight: 8 }}
+          onClick={() => {
+            setEditingRedemption({
+              id: undefined,
+            });
+            setShowEdit(true);
+          }}
         >
           {t('添加兑换码')}
         </Button>
         <Button
-            label={t('复制所选兑换码')}
-            type='warning'
-            onClick={async () => {
-              if (selectedKeys.length === 0) {
-                showError(t('请至少选择一个兑换码!'));
-                return;
-              }
-              let keys = '';
-              for (let i = 0; i < selectedKeys.length; i++) {
-                keys += selectedKeys[i].name + '    ' + selectedKeys[i].key + '\n';
-              }
-              await copyText(keys);
-            }}
+          label={t('复制所选兑换码')}
+          type='warning'
+          onClick={async () => {
+            if (selectedKeys.length === 0) {
+              showError(t('请至少选择一个兑换码!'));
+              return;
+            }
+            let keys = '';
+            for (let i = 0; i < selectedKeys.length; i++) {
+              keys +=
+                selectedKeys[i].name + '    ' + selectedKeys[i].key + '\n';
+            }
+            await copyText(keys);
+          }}
         >
           {t('复制所选兑换码到剪贴板')}
         </Button>
@@ -417,7 +425,7 @@ const RedemptionsTable = () => {
             t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
               start: page.currentStart,
               end: page.currentEnd,
-              total: tokenCount
+              total: tokenCount,
             }),
           onPageSizeChange: (size) => {
             setPageSize(size);

+ 59 - 34
web/src/components/RegisterForm.js

@@ -1,13 +1,32 @@
 import React, { useContext, useEffect, useState } from 'react';
 import { Link, useNavigate } from 'react-router-dom';
-import { API, getLogo, showError, showInfo, showSuccess, updateAPI } from '../helpers';
+import {
+  API,
+  getLogo,
+  showError,
+  showInfo,
+  showSuccess,
+  updateAPI,
+} from '../helpers';
 import Turnstile from 'react-turnstile';
-import { Button, Card, Divider, Form, Icon, Layout, Modal } from '@douyinfe/semi-ui';
+import {
+  Button,
+  Card,
+  Divider,
+  Form,
+  Icon,
+  Layout,
+  Modal,
+} from '@douyinfe/semi-ui';
 import Title from '@douyinfe/semi-ui/lib/es/typography/title';
 import Text from '@douyinfe/semi-ui/lib/es/typography/text';
 import { IconGithubLogo } from '@douyinfe/semi-icons';
-import {onGitHubOAuthClicked, onLinuxDOOAuthClicked, onOIDCClicked} from './utils.js';
-import OIDCIcon from "./OIDCIcon.js";
+import {
+  onGitHubOAuthClicked,
+  onLinuxDOOAuthClicked,
+  onOIDCClicked,
+} from './utils.js';
+import OIDCIcon from './OIDCIcon.js';
 import LinuxDoIcon from './LinuxDoIcon.js';
 import WeChatIcon from './WeChatIcon.js';
 import TelegramLoginButton from 'react-telegram-login/src';
@@ -22,7 +41,7 @@ const RegisterForm = () => {
     password: '',
     password2: '',
     email: '',
-    verification_code: ''
+    verification_code: '',
   });
   const { username, password, password2 } = inputs;
   const [showEmailVerification, setShowEmailVerification] = useState(false);
@@ -54,7 +73,6 @@ const RegisterForm = () => {
     }
   });
 
-
   const onWeChatLoginClicked = () => {
     setShowWeChatLoginModal(true);
   };
@@ -106,7 +124,7 @@ const RegisterForm = () => {
       inputs.aff_code = affCode;
       const res = await API.post(
         `/api/user/register?turnstile=${turnstileToken}`,
-        inputs
+        inputs,
       );
       const { success, message } = res.data;
       if (success) {
@@ -127,7 +145,7 @@ const RegisterForm = () => {
     }
     setLoading(true);
     const res = await API.get(
-      `/api/verification?email=${inputs.email}&turnstile=${turnstileToken}`
+      `/api/verification?email=${inputs.email}&turnstile=${turnstileToken}`,
     );
     const { success, message } = res.data;
     if (success) {
@@ -169,7 +187,6 @@ const RegisterForm = () => {
     }
   };
 
-
   return (
     <div>
       <Layout>
@@ -179,7 +196,7 @@ const RegisterForm = () => {
             style={{
               justifyContent: 'center',
               display: 'flex',
-              marginTop: 120
+              marginTop: 120,
             }}
           >
             <div style={{ width: 500 }}>
@@ -187,28 +204,28 @@ const RegisterForm = () => {
                 <Title heading={2} style={{ textAlign: 'center' }}>
                   {t('新用户注册')}
                 </Title>
-                <Form size="large">
+                <Form size='large'>
                   <Form.Input
                     field={'username'}
                     label={t('用户名')}
                     placeholder={t('用户名')}
-                    name="username"
+                    name='username'
                     onChange={(value) => handleChange('username', value)}
                   />
                   <Form.Input
                     field={'password'}
                     label={t('密码')}
                     placeholder={t('输入密码,最短 8 位,最长 20 位')}
-                    name="password"
-                    type="password"
+                    name='password'
+                    type='password'
                     onChange={(value) => handleChange('password', value)}
                   />
                   <Form.Input
                     field={'password2'}
                     label={t('确认密码')}
                     placeholder={t('确认密码')}
-                    name="password2"
-                    type="password"
+                    name='password2'
+                    type='password'
                     onChange={(value) => handleChange('password2', value)}
                   />
                   {showEmailVerification ? (
@@ -218,10 +235,13 @@ const RegisterForm = () => {
                         label={t('邮箱')}
                         placeholder={t('输入邮箱地址')}
                         onChange={(value) => handleChange('email', value)}
-                        name="email"
-                        type="email"
+                        name='email'
+                        type='email'
                         suffix={
-                          <Button onClick={sendVerificationCode} disabled={loading}>
+                          <Button
+                            onClick={sendVerificationCode}
+                            disabled={loading}
+                          >
                             {t('获取验证码')}
                           </Button>
                         }
@@ -230,8 +250,10 @@ const RegisterForm = () => {
                         field={'verification_code'}
                         label={t('验证码')}
                         placeholder={t('输入验证码')}
-                        onChange={(value) => handleChange('verification_code', value)}
-                        name="verification_code"
+                        onChange={(value) =>
+                          handleChange('verification_code', value)
+                        }
+                        name='verification_code'
                       />
                     </>
                   ) : (
@@ -252,14 +274,12 @@ const RegisterForm = () => {
                   style={{
                     display: 'flex',
                     justifyContent: 'space-between',
-                    marginTop: 20
+                    marginTop: 20,
                   }}
                 >
                   <Text>
                     {t('已有账户?')}
-                    <Link to="/login">
-                      {t('点击登录')}
-                    </Link>
+                    <Link to='/login'>{t('点击登录')}</Link>
                   </Text>
                 </div>
                 {status.github_oauth ||
@@ -290,15 +310,18 @@ const RegisterForm = () => {
                         <></>
                       )}
                       {status.oidc_enabled ? (
-                          <Button
-                              type='primary'
-                              icon={<OIDCIcon />}
-                              onClick={() =>
-                                  onOIDCClicked(status.oidc_authorization_endpoint, status.oidc_client_id)
-                              }
-                          />
+                        <Button
+                          type='primary'
+                          icon={<OIDCIcon />}
+                          onClick={() =>
+                            onOIDCClicked(
+                              status.oidc_authorization_endpoint,
+                              status.oidc_client_id,
+                            )
+                          }
+                        />
                       ) : (
-                          <></>
+                        <></>
                       )}
                       {status.linuxdo_oauth ? (
                         <Button
@@ -365,7 +388,9 @@ const RegisterForm = () => {
                 </div>
                 <div style={{ textAlign: 'center' }}>
                   <p>
-                    {t('微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)')}
+                    {t(
+                      '微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)',
+                    )}
                   </p>
                 </div>
                 <Form size='large'>

+ 76 - 38
web/src/components/SiderBar.js

@@ -15,10 +15,13 @@ import {
 import '../index.css';
 
 import {
-  IconCalendarClock, IconChecklistStroked,
-  IconComment, IconCommentStroked,
+  IconCalendarClock,
+  IconChecklistStroked,
+  IconComment,
+  IconCommentStroked,
   IconCreditCard,
-  IconGift, IconHelpCircle,
+  IconGift,
+  IconHelpCircle,
   IconHistogram,
   IconHome,
   IconImage,
@@ -26,9 +29,16 @@ import {
   IconLayers,
   IconPriceTag,
   IconSetting,
-  IconUser
+  IconUser,
 } from '@douyinfe/semi-icons';
-import { Avatar, Dropdown, Layout, Nav, Switch, Divider } from '@douyinfe/semi-ui';
+import {
+  Avatar,
+  Dropdown,
+  Layout,
+  Nav,
+  Switch,
+  Divider,
+} from '@douyinfe/semi-ui';
 import { setStatusData } from '../helpers/data.js';
 import { stringToColor } from '../helpers/render.js';
 import { useSetTheme, useTheme } from '../context/Theme/index.js';
@@ -44,21 +54,23 @@ const navItemStyle = {
 // 自定义侧边栏按钮悬停样式
 const navItemHoverStyle = {
   backgroundColor: 'var(--semi-color-primary-light-default)',
-  color: 'var(--semi-color-primary)'
+  color: 'var(--semi-color-primary)',
 };
 
 // 自定义侧边栏按钮选中样式
 const navItemSelectedStyle = {
   backgroundColor: 'var(--semi-color-primary-light-default)',
   color: 'var(--semi-color-primary)',
-  fontWeight: '600'
+  fontWeight: '600',
 };
 
 // 自定义图标样式
 const iconStyle = (itemKey, selectedKeys) => {
   return {
     fontSize: '18px',
-    color: selectedKeys.includes(itemKey) ? 'var(--semi-color-primary)' : 'var(--semi-color-text-2)',
+    color: selectedKeys.includes(itemKey)
+      ? 'var(--semi-color-primary)'
+      : 'var(--semi-color-text-2)',
   };
 };
 
@@ -99,8 +111,24 @@ const SiderBar = () => {
 
   // 预先计算所有可能的图标样式
   const allItemKeys = useMemo(() => {
-    const keys = ['home', 'channel', 'token', 'redemption', 'topup', 'user', 'log', 'midjourney',
-                 'setting', 'about', 'chat', 'detail', 'pricing', 'task', 'playground', 'personal'];
+    const keys = [
+      'home',
+      'channel',
+      'token',
+      'redemption',
+      'topup',
+      'user',
+      'log',
+      'midjourney',
+      'setting',
+      'about',
+      'chat',
+      'detail',
+      'pricing',
+      'task',
+      'playground',
+      'personal',
+    ];
     // 添加聊天项的keys
     for (let i = 0; i < chatItems.length; i++) {
       keys.push('chat' + i);
@@ -111,7 +139,7 @@ const SiderBar = () => {
   // 使用useMemo一次性计算所有图标样式
   const iconStyles = useMemo(() => {
     const styles = {};
-    allItemKeys.forEach(key => {
+    allItemKeys.forEach((key) => {
       styles[key] = iconStyle(key, selectedKeys);
     });
     return styles;
@@ -157,10 +185,8 @@ const SiderBar = () => {
         to: '/task',
         icon: <IconChecklistStroked />,
         className:
-          localStorage.getItem('enable_task') === 'true'
-            ? ''
-            : 'tableHiddle',
-      }
+          localStorage.getItem('enable_task') === 'true' ? '' : 'tableHiddle',
+      },
     ],
     [
       localStorage.getItem('enable_data_export'),
@@ -241,13 +267,13 @@ const SiderBar = () => {
   // Function to update router map with chat routes
   const updateRouterMapWithChats = (chats) => {
     const newRouterMap = { ...routerMap };
-    
+
     if (Array.isArray(chats) && chats.length > 0) {
       for (let i = 0; i < chats.length; i++) {
         newRouterMap['chat' + i] = '/chat/' + i;
       }
     }
-    
+
     setRouterMapState(newRouterMap);
     return newRouterMap;
   };
@@ -270,13 +296,13 @@ const SiderBar = () => {
             chatItems.push(chat);
           }
           setChatItems(chatItems);
-          
+
           // Update router map with chat routes
           updateRouterMapWithChats(chats);
         }
       } catch (e) {
         console.error(e);
-        showError('聊天数据解析失败')
+        showError('聊天数据解析失败');
       }
     }
   }, []);
@@ -284,7 +310,9 @@ const SiderBar = () => {
   // Update the useEffect for route selection
   useEffect(() => {
     const currentPath = location.pathname;
-    let matchingKey = Object.keys(routerMapState).find(key => routerMapState[key] === currentPath);
+    let matchingKey = Object.keys(routerMapState).find(
+      (key) => routerMapState[key] === currentPath,
+    );
 
     // Handle chat routes
     if (!matchingKey && currentPath.startsWith('/chat/')) {
@@ -325,8 +353,8 @@ const SiderBar = () => {
   return (
     <>
       <Nav
-        className="custom-sidebar-nav"
-        style={{ 
+        className='custom-sidebar-nav'
+        style={{
           width: isCollapsed ? '60px' : '200px',
           boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
           borderRight: '1px solid var(--semi-color-border)',
@@ -351,7 +379,9 @@ const SiderBar = () => {
           // 确保在收起侧边栏时有选中的项目,避免不必要的计算
           if (selectedKeys.length === 0) {
             const currentPath = location.pathname;
-            const matchingKey = Object.keys(routerMapState).find(key => routerMapState[key] === currentPath);
+            const matchingKey = Object.keys(routerMapState).find(
+              (key) => routerMapState[key] === currentPath,
+            );
 
             if (matchingKey) {
               setSelectedKeys([matchingKey]);
@@ -382,12 +412,12 @@ const SiderBar = () => {
           } else {
             styleDispatch({ type: 'SET_INNER_PADDING', payload: true });
           }
-          
+
           // 如果点击的是已经展开的子菜单的父项,则收起子菜单
           if (openedKeys.includes(key.itemKey)) {
-            setOpenedKeys(openedKeys.filter(k => k !== key.itemKey));
+            setOpenedKeys(openedKeys.filter((k) => k !== key.itemKey));
           }
-          
+
           setSelectedKeys([key.itemKey]);
         }}
         openKeys={openedKeys}
@@ -403,7 +433,9 @@ const SiderBar = () => {
                 key={item.itemKey}
                 itemKey={item.itemKey}
                 text={item.text}
-                icon={React.cloneElement(item.icon, { style: iconStyles[item.itemKey] })}
+                icon={React.cloneElement(item.icon, {
+                  style: iconStyles[item.itemKey],
+                })}
               >
                 {item.items.map((subItem) => (
                   <Nav.Item
@@ -420,7 +452,9 @@ const SiderBar = () => {
                 key={item.itemKey}
                 itemKey={item.itemKey}
                 text={item.text}
-                icon={React.cloneElement(item.icon, { style: iconStyles[item.itemKey] })}
+                icon={React.cloneElement(item.icon, {
+                  style: iconStyles[item.itemKey],
+                })}
               />
             );
           }
@@ -436,7 +470,9 @@ const SiderBar = () => {
             key={item.itemKey}
             itemKey={item.itemKey}
             text={item.text}
-            icon={React.cloneElement(item.icon, { style: iconStyles[item.itemKey] })}
+            icon={React.cloneElement(item.icon, {
+              style: iconStyles[item.itemKey],
+            })}
             className={item.className}
           />
         ))}
@@ -453,7 +489,9 @@ const SiderBar = () => {
                 key={item.itemKey}
                 itemKey={item.itemKey}
                 text={item.text}
-                icon={React.cloneElement(item.icon, { style: iconStyles[item.itemKey] })}
+                icon={React.cloneElement(item.icon, {
+                  style: iconStyles[item.itemKey],
+                })}
                 className={item.className}
               />
             ))}
@@ -470,7 +508,9 @@ const SiderBar = () => {
             key={item.itemKey}
             itemKey={item.itemKey}
             text={item.text}
-            icon={React.cloneElement(item.icon, { style: iconStyles[item.itemKey] })}
+            icon={React.cloneElement(item.icon, {
+              style: iconStyles[item.itemKey],
+            })}
             className={item.className}
           />
         ))}
@@ -480,14 +520,12 @@ const SiderBar = () => {
             paddingBottom: styleState?.isMobile ? '112px' : '',
           }}
           collapseButton={true}
-          collapseText={(collapsed)=>
-            {
-              if(collapsed){
-                return t('展开侧边栏')
-              }
-                return t('收起侧边栏')
+          collapseText={(collapsed) => {
+            if (collapsed) {
+              return t('展开侧边栏');
             }
-          }
+            return t('收起侧边栏');
+          }}
         />
       </Nav>
     </>

+ 707 - 545
web/src/components/SystemSetting.js

@@ -9,6 +9,7 @@ import {
   Banner,
   TagInput,
   Spin,
+  Card,
 } from '@douyinfe/semi-ui';
 const { Text } = Typography;
 import {
@@ -77,7 +78,8 @@ const SystemSetting = () => {
   const [isLoaded, setIsLoaded] = useState(false);
   const formApiRef = useRef(null);
   const [emailDomainWhitelist, setEmailDomainWhitelist] = useState([]);
-  const [showPasswordLoginConfirmModal, setShowPasswordLoginConfirmModal] = useState(false);
+  const [showPasswordLoginConfirmModal, setShowPasswordLoginConfirmModal] =
+    useState(false);
   const [linuxDOOAuthEnabled, setLinuxDOOAuthEnabled] = useState(false);
 
   const getOptions = async () => {
@@ -138,18 +140,18 @@ const SystemSetting = () => {
     setLoading(true);
     try {
       // 分离 checkbox 类型的选项和其他选项
-      const checkboxOptions = options.filter(opt => 
-        opt.key.toLowerCase().endsWith('enabled')
+      const checkboxOptions = options.filter((opt) =>
+        opt.key.toLowerCase().endsWith('enabled'),
       );
-      const otherOptions = options.filter(opt => 
-        !opt.key.toLowerCase().endsWith('enabled')
+      const otherOptions = options.filter(
+        (opt) => !opt.key.toLowerCase().endsWith('enabled'),
       );
 
       // 处理 checkbox 类型的选项
       for (const opt of checkboxOptions) {
         const res = await API.put('/api/option/', {
           key: opt.key,
-          value: opt.value.toString()
+          value: opt.value.toString(),
         });
         if (!res.data.success) {
           showError(res.data.message);
@@ -159,18 +161,19 @@ const SystemSetting = () => {
 
       // 处理其他选项
       if (otherOptions.length > 0) {
-        const requestQueue = otherOptions.map(opt => 
+        const requestQueue = otherOptions.map((opt) =>
           API.put('/api/option/', {
             key: opt.key,
-            value: typeof opt.value === 'boolean' ? opt.value.toString() : opt.value
-          })
+            value:
+              typeof opt.value === 'boolean' ? opt.value.toString() : opt.value,
+          }),
         );
 
         const results = await Promise.all(requestQueue);
-        
+
         // 检查所有请求是否成功
-        const errorResults = results.filter(res => !res.data.success);
-        errorResults.forEach(res => {
+        const errorResults = results.filter((res) => !res.data.success);
+        errorResults.forEach((res) => {
           showError(res.data.message);
         });
       }
@@ -178,7 +181,7 @@ const SystemSetting = () => {
       showSuccess('更新成功');
       // 更新本地状态
       const newInputs = { ...inputs };
-      options.forEach(opt => {
+      options.forEach((opt) => {
         newInputs[opt.key] = opt.value;
       });
       setInputs(newInputs);
@@ -201,7 +204,7 @@ const SystemSetting = () => {
     let WorkerUrl = removeTrailingSlash(inputs.WorkerUrl);
     await updateOptions([
       { key: 'WorkerUrl', value: WorkerUrl },
-      { key: 'WorkerValidKey', value: inputs.WorkerValidKey }
+      { key: 'WorkerValidKey', value: inputs.WorkerValidKey },
     ]);
   };
 
@@ -216,11 +219,11 @@ const SystemSetting = () => {
         return;
       }
     }
-    
+
     const options = [
-      { key: 'PayAddress', value: removeTrailingSlash(inputs.PayAddress) }
+      { key: 'PayAddress', value: removeTrailingSlash(inputs.PayAddress) },
     ];
-    
+
     if (inputs.EpayId !== '') {
       options.push({ key: 'EpayId', value: inputs.EpayId });
     }
@@ -234,18 +237,21 @@ const SystemSetting = () => {
       options.push({ key: 'MinTopUp', value: inputs.MinTopUp.toString() });
     }
     if (inputs.CustomCallbackAddress !== '') {
-      options.push({ key: 'CustomCallbackAddress', value: inputs.CustomCallbackAddress });
+      options.push({
+        key: 'CustomCallbackAddress',
+        value: inputs.CustomCallbackAddress,
+      });
     }
     if (originInputs['TopupGroupRatio'] !== inputs.TopupGroupRatio) {
       options.push({ key: 'TopupGroupRatio', value: inputs.TopupGroupRatio });
     }
-    
+
     await updateOptions(options);
   };
 
   const submitSMTP = async () => {
     const options = [];
-    
+
     if (originInputs['SMTPServer'] !== inputs.SMTPServer) {
       options.push({ key: 'SMTPServer', value: inputs.SMTPServer });
     }
@@ -255,13 +261,19 @@ const SystemSetting = () => {
     if (originInputs['SMTPFrom'] !== inputs.SMTPFrom) {
       options.push({ key: 'SMTPFrom', value: inputs.SMTPFrom });
     }
-    if (originInputs['SMTPPort'] !== inputs.SMTPPort && inputs.SMTPPort !== '') {
+    if (
+      originInputs['SMTPPort'] !== inputs.SMTPPort &&
+      inputs.SMTPPort !== ''
+    ) {
       options.push({ key: 'SMTPPort', value: inputs.SMTPPort });
     }
-    if (originInputs['SMTPToken'] !== inputs.SMTPToken && inputs.SMTPToken !== '') {
+    if (
+      originInputs['SMTPToken'] !== inputs.SMTPToken &&
+      inputs.SMTPToken !== ''
+    ) {
       options.push({ key: 'SMTPToken', value: inputs.SMTPToken });
     }
-    
+
     if (options.length > 0) {
       await updateOptions(options);
     }
@@ -269,10 +281,12 @@ const SystemSetting = () => {
 
   const submitEmailDomainWhitelist = async () => {
     if (Array.isArray(emailDomainWhitelist)) {
-      await updateOptions([{ 
-        key: 'EmailDomainWhitelist', 
-        value: emailDomainWhitelist.join(',') 
-      }]);
+      await updateOptions([
+        {
+          key: 'EmailDomainWhitelist',
+          value: emailDomainWhitelist.join(','),
+        },
+      ]);
     } else {
       showError('邮箱域名白名单格式不正确');
     }
@@ -280,23 +294,32 @@ const SystemSetting = () => {
 
   const submitWeChat = async () => {
     const options = [];
-    
+
     if (originInputs['WeChatServerAddress'] !== inputs.WeChatServerAddress) {
-      options.push({ 
-        key: 'WeChatServerAddress', 
-        value: removeTrailingSlash(inputs.WeChatServerAddress) 
+      options.push({
+        key: 'WeChatServerAddress',
+        value: removeTrailingSlash(inputs.WeChatServerAddress),
       });
     }
-    if (originInputs['WeChatAccountQRCodeImageURL'] !== inputs.WeChatAccountQRCodeImageURL) {
-      options.push({ 
-        key: 'WeChatAccountQRCodeImageURL', 
-        value: inputs.WeChatAccountQRCodeImageURL 
+    if (
+      originInputs['WeChatAccountQRCodeImageURL'] !==
+      inputs.WeChatAccountQRCodeImageURL
+    ) {
+      options.push({
+        key: 'WeChatAccountQRCodeImageURL',
+        value: inputs.WeChatAccountQRCodeImageURL,
       });
     }
-    if (originInputs['WeChatServerToken'] !== inputs.WeChatServerToken && inputs.WeChatServerToken !== '') {
-      options.push({ key: 'WeChatServerToken', value: inputs.WeChatServerToken });
+    if (
+      originInputs['WeChatServerToken'] !== inputs.WeChatServerToken &&
+      inputs.WeChatServerToken !== ''
+    ) {
+      options.push({
+        key: 'WeChatServerToken',
+        value: inputs.WeChatServerToken,
+      });
     }
-    
+
     if (options.length > 0) {
       await updateOptions(options);
     }
@@ -304,14 +327,20 @@ const SystemSetting = () => {
 
   const submitGitHubOAuth = async () => {
     const options = [];
-    
+
     if (originInputs['GitHubClientId'] !== inputs.GitHubClientId) {
       options.push({ key: 'GitHubClientId', value: inputs.GitHubClientId });
     }
-    if (originInputs['GitHubClientSecret'] !== inputs.GitHubClientSecret && inputs.GitHubClientSecret !== '') {
-      options.push({ key: 'GitHubClientSecret', value: inputs.GitHubClientSecret });
+    if (
+      originInputs['GitHubClientSecret'] !== inputs.GitHubClientSecret &&
+      inputs.GitHubClientSecret !== ''
+    ) {
+      options.push({
+        key: 'GitHubClientSecret',
+        value: inputs.GitHubClientSecret,
+      });
     }
-    
+
     if (options.length > 0) {
       await updateOptions(options);
     }
@@ -319,44 +348,74 @@ const SystemSetting = () => {
 
   const submitOIDCSettings = async () => {
     if (inputs['oidc.well_known'] !== '') {
-      if (!inputs['oidc.well_known'].startsWith('http://') && !inputs['oidc.well_known'].startsWith('https://')) {
+      if (
+        !inputs['oidc.well_known'].startsWith('http://') &&
+        !inputs['oidc.well_known'].startsWith('https://')
+      ) {
         showError('Well-Known URL 必须以 http:// 或 https:// 开头');
         return;
       }
       try {
         const res = await API.get(inputs['oidc.well_known']);
-        inputs['oidc.authorization_endpoint'] = res.data['authorization_endpoint'];
+        inputs['oidc.authorization_endpoint'] =
+          res.data['authorization_endpoint'];
         inputs['oidc.token_endpoint'] = res.data['token_endpoint'];
         inputs['oidc.user_info_endpoint'] = res.data['userinfo_endpoint'];
         showSuccess('获取 OIDC 配置成功!');
       } catch (err) {
         console.error(err);
-        showError("获取 OIDC 配置失败,请检查网络状况和 Well-Known URL 是否正确");
+        showError(
+          '获取 OIDC 配置失败,请检查网络状况和 Well-Known URL 是否正确',
+        );
         return;
       }
     }
 
     const options = [];
-    
+
     if (originInputs['oidc.well_known'] !== inputs['oidc.well_known']) {
-      options.push({ key: 'oidc.well_known', value: inputs['oidc.well_known'] });
+      options.push({
+        key: 'oidc.well_known',
+        value: inputs['oidc.well_known'],
+      });
     }
     if (originInputs['oidc.client_id'] !== inputs['oidc.client_id']) {
       options.push({ key: 'oidc.client_id', value: inputs['oidc.client_id'] });
     }
-    if (originInputs['oidc.client_secret'] !== inputs['oidc.client_secret'] && inputs['oidc.client_secret'] !== '') {
-      options.push({ key: 'oidc.client_secret', value: inputs['oidc.client_secret'] });
+    if (
+      originInputs['oidc.client_secret'] !== inputs['oidc.client_secret'] &&
+      inputs['oidc.client_secret'] !== ''
+    ) {
+      options.push({
+        key: 'oidc.client_secret',
+        value: inputs['oidc.client_secret'],
+      });
     }
-    if (originInputs['oidc.authorization_endpoint'] !== inputs['oidc.authorization_endpoint']) {
-      options.push({ key: 'oidc.authorization_endpoint', value: inputs['oidc.authorization_endpoint'] });
+    if (
+      originInputs['oidc.authorization_endpoint'] !==
+      inputs['oidc.authorization_endpoint']
+    ) {
+      options.push({
+        key: 'oidc.authorization_endpoint',
+        value: inputs['oidc.authorization_endpoint'],
+      });
     }
     if (originInputs['oidc.token_endpoint'] !== inputs['oidc.token_endpoint']) {
-      options.push({ key: 'oidc.token_endpoint', value: inputs['oidc.token_endpoint'] });
+      options.push({
+        key: 'oidc.token_endpoint',
+        value: inputs['oidc.token_endpoint'],
+      });
     }
-    if (originInputs['oidc.user_info_endpoint'] !== inputs['oidc.user_info_endpoint']) {
-      options.push({ key: 'oidc.user_info_endpoint', value: inputs['oidc.user_info_endpoint'] });
+    if (
+      originInputs['oidc.user_info_endpoint'] !==
+      inputs['oidc.user_info_endpoint']
+    ) {
+      options.push({
+        key: 'oidc.user_info_endpoint',
+        value: inputs['oidc.user_info_endpoint'],
+      });
     }
-    
+
     if (options.length > 0) {
       await updateOptions(options);
     }
@@ -365,21 +424,27 @@ const SystemSetting = () => {
   const submitTelegramSettings = async () => {
     const options = [
       { key: 'TelegramBotToken', value: inputs.TelegramBotToken },
-      { key: 'TelegramBotName', value: inputs.TelegramBotName }
+      { key: 'TelegramBotName', value: inputs.TelegramBotName },
     ];
     await updateOptions(options);
   };
 
   const submitTurnstile = async () => {
     const options = [];
-    
+
     if (originInputs['TurnstileSiteKey'] !== inputs.TurnstileSiteKey) {
       options.push({ key: 'TurnstileSiteKey', value: inputs.TurnstileSiteKey });
     }
-    if (originInputs['TurnstileSecretKey'] !== inputs.TurnstileSecretKey && inputs.TurnstileSecretKey !== '') {
-      options.push({ key: 'TurnstileSecretKey', value: inputs.TurnstileSecretKey });
+    if (
+      originInputs['TurnstileSecretKey'] !== inputs.TurnstileSecretKey &&
+      inputs.TurnstileSecretKey !== ''
+    ) {
+      options.push({
+        key: 'TurnstileSecretKey',
+        value: inputs.TurnstileSecretKey,
+      });
     }
-    
+
     if (options.length > 0) {
       await updateOptions(options);
     }
@@ -387,14 +452,20 @@ const SystemSetting = () => {
 
   const submitLinuxDOOAuth = async () => {
     const options = [];
-    
+
     if (originInputs['LinuxDOClientId'] !== inputs.LinuxDOClientId) {
       options.push({ key: 'LinuxDOClientId', value: inputs.LinuxDOClientId });
     }
-    if (originInputs['LinuxDOClientSecret'] !== inputs.LinuxDOClientSecret && inputs.LinuxDOClientSecret !== '') {
-      options.push({ key: 'LinuxDOClientSecret', value: inputs.LinuxDOClientSecret });
+    if (
+      originInputs['LinuxDOClientSecret'] !== inputs.LinuxDOClientSecret &&
+      inputs.LinuxDOClientSecret !== ''
+    ) {
+      options.push({
+        key: 'LinuxDOClientSecret',
+        value: inputs.LinuxDOClientSecret,
+      });
     }
-    
+
     if (options.length > 0) {
       await updateOptions(options);
     }
@@ -402,7 +473,7 @@ const SystemSetting = () => {
 
   const handleCheckboxChange = async (optionKey, event) => {
     const value = event.target.checked;
-    
+
     if (optionKey === 'PasswordLoginEnabled' && !value) {
       setShowPasswordLoginConfirmModal(true);
     } else {
@@ -419,7 +490,7 @@ const SystemSetting = () => {
   };
 
   return (
-    <div style={{ padding: '20px' }}>
+    <div>
       {isLoaded ? (
         <Form
           initValues={inputs}
@@ -427,504 +498,595 @@ const SystemSetting = () => {
           getFormApi={(api) => (formApiRef.current = api)}
         >
           {({ formState, values, formApi }) => (
-            <>
-              <Form.Section text='通用设置'>
-                <Form.Input
-                  field='ServerAddress'
-                  label='服务器地址'
-                  placeholder='例如:https://yourdomain.com'
-                  style={{ width: '100%' }}
-                />
-                <Button onClick={submitServerAddress}>更新服务器地址</Button>
-              </Form.Section>
-
-              <Form.Section text='代理设置'>
-                <Text>
-                  (支持{' '}
-                  <a
-                    href='https://github.com/Calcium-Ion/new-api-worker'
-                    target='_blank'
-                    rel='noreferrer'
-                  >
-                    new-api-worker
-                  </a>
-                  )
-                </Text>
-                <Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
-                  <Col xs={24} sm={24} md={12} lg={12} xl={12}>
-                    <Form.Input
-                      field='WorkerUrl'
-                      label='Worker地址'
-                      placeholder='例如:https://workername.yourdomain.workers.dev'
-                    />
-                  </Col>
-                  <Col xs={24} sm={24} md={12} lg={12} xl={12}>
-                    <Form.Input
-                      field='WorkerValidKey'
-                      label='Worker密钥'
-                      placeholder='敏感信息不会发送到前端显示'
-                      type='password'
-                    />
-                  </Col>
-                </Row>
-                <Button onClick={submitWorker}>更新Worker设置</Button>
-              </Form.Section>
-
-              <Form.Section text='支付设置'>
-                <Text>
-                  (当前仅支持易支付接口,默认使用上方服务器地址作为回调地址!)
-                </Text>
-                <Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
-                  <Col xs={24} sm={24} md={8} lg={8} xl={8}>
-                    <Form.Input
-                      field='PayAddress'
-                      label='支付地址'
-                      placeholder='例如:https://yourdomain.com'
-                    />
-                  </Col>
-                  <Col xs={24} sm={24} md={8} lg={8} xl={8}>
-                    <Form.Input
-                      field='EpayId'
-                      label='易支付商户ID'
-                      placeholder='例如:0001'
-                    />
-                  </Col>
-                  <Col xs={24} sm={24} md={8} lg={8} xl={8}>
-                    <Form.Input
-                      field='EpayKey'
-                      label='易支付商户密钥'
-                      placeholder='敏感信息不会发送到前端显示'
-                      type='password'
-                    />
-                  </Col>
-                </Row>
-                <Row
-                  gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
-                  style={{ marginTop: 16 }}
-                >
-                  <Col xs={24} sm={24} md={8} lg={8} xl={8}>
-                    <Form.Input
-                      field='CustomCallbackAddress'
-                      label='回调地址'
-                      placeholder='例如:https://yourdomain.com'
-                    />
-                  </Col>
-                  <Col xs={24} sm={24} md={8} lg={8} xl={8}>
-                    <Form.InputNumber
-                      field='Price'
-                      precision={2}
-                      label='充值价格(x元/美金)'
-                      placeholder='例如:7,就是7元/美金'
-                    />
-                  </Col>
-                  <Col xs={24} sm={24} md={8} lg={8} xl={8}>
-                    <Form.InputNumber
-                      field='MinTopUp'
-                      label='最低充值美元数量'
-                      placeholder='例如:2,就是最低充值2$'
-                    />
-                  </Col>
-                </Row>
-                <Form.TextArea
-                  field='TopupGroupRatio'
-                  label='充值分组倍率'
-                  placeholder='为一个 JSON 文本,键为组名称,值为倍率'
-                  autosize
-                />
-                <Button onClick={submitPayAddress}>更新支付设置</Button>
-              </Form.Section>
-
-              <Form.Section text='配置登录注册'>
-                <Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
-                  <Col xs={24} sm={24} md={12} lg={12} xl={12}>
-                    <Form.Checkbox
-                      field='PasswordLoginEnabled'
-                      noLabel
-                      onChange={(e) =>
-                        handleCheckboxChange('PasswordLoginEnabled', e)
-                      }
-                    >
-                      允许通过密码进行登录
-                    </Form.Checkbox>
-                    <Form.Checkbox
-                      field='PasswordRegisterEnabled'
-                      noLabel
-                      onChange={(e) =>
-                        handleCheckboxChange('PasswordRegisterEnabled', e)
-                      }
-                    >
-                      允许通过密码进行注册
-                    </Form.Checkbox>
-                    <Form.Checkbox
-                      field='EmailVerificationEnabled'
-                      noLabel
-                      onChange={(e) =>
-                        handleCheckboxChange('EmailVerificationEnabled', e)
-                      }
-                    >
-                      通过密码注册时需要进行邮箱验证
-                    </Form.Checkbox>
-                    <Form.Checkbox
-                      field='RegisterEnabled'
-                      noLabel
-                      onChange={(e) => handleCheckboxChange('RegisterEnabled', e)}
+            <div
+              style={{
+                display: 'flex',
+                flexDirection: 'column',
+                gap: '10px',
+                marginTop: '10px',
+              }}
+            >
+              <Card>
+                <Form.Section text='通用设置'>
+                  <Form.Input
+                    field='ServerAddress'
+                    label='服务器地址'
+                    placeholder='例如:https://yourdomain.com'
+                    style={{ width: '100%' }}
+                  />
+                  <Button onClick={submitServerAddress}>更新服务器地址</Button>
+                </Form.Section>
+              </Card>
+              <Card>
+                <Form.Section text='代理设置'>
+                  <Text>
+                    (支持{' '}
+                    <a
+                      href='https://github.com/Calcium-Ion/new-api-worker'
+                      target='_blank'
+                      rel='noreferrer'
                     >
-                      允许新用户注册
-                    </Form.Checkbox>
-                    <Form.Checkbox
-                      field='TurnstileCheckEnabled'
-                      noLabel
-                      onChange={(e) =>
-                        handleCheckboxChange('TurnstileCheckEnabled', e)
-                      }
-                    >
-                      启用 Turnstile 用户校验
-                    </Form.Checkbox>
-                  </Col>
-                  <Col xs={24} sm={24} md={12} lg={12} xl={12}>
-                    <Form.Checkbox
-                      field='GitHubOAuthEnabled'
-                      noLabel
-                      onChange={(e) =>
-                        handleCheckboxChange('GitHubOAuthEnabled', e)
-                      }
-                    >
-                      允许通过 GitHub 账户登录 & 注册
-                    </Form.Checkbox>
-                    <Form.Checkbox
-                      field='LinuxDOOAuthEnabled'
-                      noLabel
-                      onChange={(e) =>
-                        handleCheckboxChange('LinuxDOOAuthEnabled', e)
-                      }
-                    >
-                      允许通过 Linux DO 账户登录 & 注册
-                    </Form.Checkbox>
-                    <Form.Checkbox
-                      field='WeChatAuthEnabled'
-                      noLabel
-                      onChange={(e) =>
-                        handleCheckboxChange('WeChatAuthEnabled', e)
-                      }
-                    >
-                      允许通过微信登录 & 注册
-                    </Form.Checkbox>
-                    <Form.Checkbox
-                      field='TelegramOAuthEnabled'
-                      noLabel
-                      onChange={(e) =>
-                        handleCheckboxChange('TelegramOAuthEnabled', e)
-                      }
-                    >
-                      允许通过 Telegram 进行登录
-                    </Form.Checkbox>
-                    <Form.Checkbox
-                      field="['oidc.enabled']"
-                      noLabel
-                      onChange={(e) => handleCheckboxChange('oidc.enabled', e)}
-                    >
-                      允许通过 OIDC 进行登录
-                    </Form.Checkbox>
-                  </Col>
-                </Row>
-              </Form.Section>
-
-              <Form.Section text='配置邮箱域名白名单'>
-                <Text>用以防止恶意用户利用临时邮箱批量注册</Text>
-                <Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
-                  <Col xs={24} sm={24} md={12} lg={12} xl={12}>
-                    <Form.Checkbox
-                      field='EmailDomainRestrictionEnabled'
-                      noLabel
-                      onChange={(e) =>
-                        handleCheckboxChange('EmailDomainRestrictionEnabled', e)
-                      }
-                    >
-                      启用邮箱域名白名单
-                    </Form.Checkbox>
-                  </Col>
-                  <Col xs={24} sm={24} md={12} lg={12} xl={12}>
-                    <Form.Checkbox
-                      field='EmailAliasRestrictionEnabled'
-                      noLabel
-                      onChange={(e) =>
-                        handleCheckboxChange('EmailAliasRestrictionEnabled', e)
-                      }
-                    >
-                      启用邮箱别名限制
-                    </Form.Checkbox>
-                  </Col>
-                </Row>
-                <TagInput
-                  value={emailDomainWhitelist}
-                  onChange={setEmailDomainWhitelist}
-                  placeholder='输入域名后回车'
-                  style={{ width: '100%', marginTop: 16 }}
-                />
-                <Button
-                  onClick={submitEmailDomainWhitelist}
-                  style={{ marginTop: 10 }}
-                >
-                  保存邮箱域名白名单设置
-                </Button>
-              </Form.Section>
-
-              <Form.Section text='配置 SMTP'>
-                <Text>用以支持系统的邮件发送</Text>
-                <Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
-                  <Col xs={24} sm={24} md={8} lg={8} xl={8}>
-                    <Form.Input field='SMTPServer' label='SMTP 服务器地址' />
-                  </Col>
-                  <Col xs={24} sm={24} md={8} lg={8} xl={8}>
-                    <Form.Input field='SMTPPort' label='SMTP 端口' />
-                  </Col>
-                  <Col xs={24} sm={24} md={8} lg={8} xl={8}>
-                    <Form.Input field='SMTPAccount' label='SMTP 账户' />
-                  </Col>
-                </Row>
-                <Row
-                  gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
-                  style={{ marginTop: 16 }}
-                >
-                  <Col xs={24} sm={24} md={8} lg={8} xl={8}>
-                    <Form.Input field='SMTPFrom' label='SMTP 发送者邮箱' />
-                  </Col>
-                  <Col xs={24} sm={24} md={8} lg={8} xl={8}>
-                    <Form.Input
-                      field='SMTPToken'
-                      label='SMTP 访问凭证'
-                      type='password'
-                      placeholder='敏感信息不会发送到前端显示'
-                    />
-                  </Col>
-                  <Col xs={24} sm={24} md={8} lg={8} xl={8}>
-                    <Form.Checkbox
-                      field='SMTPSSLEnabled'
-                      noLabel
-                      onChange={(e) => handleCheckboxChange('SMTPSSLEnabled', e)}
+                      new-api-worker
+                    </a>
+                    )
+                  </Text>
+                  <Row
+                    gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
+                  >
+                    <Col xs={24} sm={24} md={12} lg={12} xl={12}>
+                      <Form.Input
+                        field='WorkerUrl'
+                        label='Worker地址'
+                        placeholder='例如:https://workername.yourdomain.workers.dev'
+                      />
+                    </Col>
+                    <Col xs={24} sm={24} md={12} lg={12} xl={12}>
+                      <Form.Input
+                        field='WorkerValidKey'
+                        label='Worker密钥'
+                        placeholder='敏感信息不会发送到前端显示'
+                        type='password'
+                      />
+                    </Col>
+                  </Row>
+                  <Button onClick={submitWorker}>更新Worker设置</Button>
+                </Form.Section>
+              </Card>
+
+              <Card>
+                <Form.Section text='支付设置'>
+                  <Text>
+                    (当前仅支持易支付接口,默认使用上方服务器地址作为回调地址!)
+                  </Text>
+                  <Row
+                    gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
+                  >
+                    <Col xs={24} sm={24} md={8} lg={8} xl={8}>
+                      <Form.Input
+                        field='PayAddress'
+                        label='支付地址'
+                        placeholder='例如:https://yourdomain.com'
+                      />
+                    </Col>
+                    <Col xs={24} sm={24} md={8} lg={8} xl={8}>
+                      <Form.Input
+                        field='EpayId'
+                        label='易支付商户ID'
+                        placeholder='例如:0001'
+                      />
+                    </Col>
+                    <Col xs={24} sm={24} md={8} lg={8} xl={8}>
+                      <Form.Input
+                        field='EpayKey'
+                        label='易支付商户密钥'
+                        placeholder='敏感信息不会发送到前端显示'
+                        type='password'
+                      />
+                    </Col>
+                  </Row>
+                  <Row
+                    gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
+                    style={{ marginTop: 16 }}
+                  >
+                    <Col xs={24} sm={24} md={8} lg={8} xl={8}>
+                      <Form.Input
+                        field='CustomCallbackAddress'
+                        label='回调地址'
+                        placeholder='例如:https://yourdomain.com'
+                      />
+                    </Col>
+                    <Col xs={24} sm={24} md={8} lg={8} xl={8}>
+                      <Form.InputNumber
+                        field='Price'
+                        precision={2}
+                        label='充值价格(x元/美金)'
+                        placeholder='例如:7,就是7元/美金'
+                      />
+                    </Col>
+                    <Col xs={24} sm={24} md={8} lg={8} xl={8}>
+                      <Form.InputNumber
+                        field='MinTopUp'
+                        label='最低充值美元数量'
+                        placeholder='例如:2,就是最低充值2$'
+                      />
+                    </Col>
+                  </Row>
+                  <Form.TextArea
+                    field='TopupGroupRatio'
+                    label='充值分组倍率'
+                    placeholder='为一个 JSON 文本,键为组名称,值为倍率'
+                    autosize
+                  />
+                  <Button onClick={submitPayAddress}>更新支付设置</Button>
+                </Form.Section>
+              </Card>
+
+              <Card>
+                <Form.Section text='配置登录注册'>
+                  <Row
+                    gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
+                  >
+                    <Col xs={24} sm={24} md={12} lg={12} xl={12}>
+                      <Form.Checkbox
+                        field='PasswordLoginEnabled'
+                        noLabel
+                        onChange={(e) =>
+                          handleCheckboxChange('PasswordLoginEnabled', e)
+                        }
+                      >
+                        允许通过密码进行登录
+                      </Form.Checkbox>
+                      <Form.Checkbox
+                        field='PasswordRegisterEnabled'
+                        noLabel
+                        onChange={(e) =>
+                          handleCheckboxChange('PasswordRegisterEnabled', e)
+                        }
+                      >
+                        允许通过密码进行注册
+                      </Form.Checkbox>
+                      <Form.Checkbox
+                        field='EmailVerificationEnabled'
+                        noLabel
+                        onChange={(e) =>
+                          handleCheckboxChange('EmailVerificationEnabled', e)
+                        }
+                      >
+                        通过密码注册时需要进行邮箱验证
+                      </Form.Checkbox>
+                      <Form.Checkbox
+                        field='RegisterEnabled'
+                        noLabel
+                        onChange={(e) =>
+                          handleCheckboxChange('RegisterEnabled', e)
+                        }
+                      >
+                        允许新用户注册
+                      </Form.Checkbox>
+                      <Form.Checkbox
+                        field='TurnstileCheckEnabled'
+                        noLabel
+                        onChange={(e) =>
+                          handleCheckboxChange('TurnstileCheckEnabled', e)
+                        }
+                      >
+                        启用 Turnstile 用户校验
+                      </Form.Checkbox>
+                    </Col>
+                    <Col xs={24} sm={24} md={12} lg={12} xl={12}>
+                      <Form.Checkbox
+                        field='GitHubOAuthEnabled'
+                        noLabel
+                        onChange={(e) =>
+                          handleCheckboxChange('GitHubOAuthEnabled', e)
+                        }
+                      >
+                        允许通过 GitHub 账户登录 & 注册
+                      </Form.Checkbox>
+                      <Form.Checkbox
+                        field='LinuxDOOAuthEnabled'
+                        noLabel
+                        onChange={(e) =>
+                          handleCheckboxChange('LinuxDOOAuthEnabled', e)
+                        }
+                      >
+                        允许通过 Linux DO 账户登录 & 注册
+                      </Form.Checkbox>
+                      <Form.Checkbox
+                        field='WeChatAuthEnabled'
+                        noLabel
+                        onChange={(e) =>
+                          handleCheckboxChange('WeChatAuthEnabled', e)
+                        }
+                      >
+                        允许通过微信登录 & 注册
+                      </Form.Checkbox>
+                      <Form.Checkbox
+                        field='TelegramOAuthEnabled'
+                        noLabel
+                        onChange={(e) =>
+                          handleCheckboxChange('TelegramOAuthEnabled', e)
+                        }
+                      >
+                        允许通过 Telegram 进行登录
+                      </Form.Checkbox>
+                      <Form.Checkbox
+                        field="['oidc.enabled']"
+                        noLabel
+                        onChange={(e) =>
+                          handleCheckboxChange('oidc.enabled', e)
+                        }
+                      >
+                        允许通过 OIDC 进行登录
+                      </Form.Checkbox>
+                    </Col>
+                  </Row>
+                </Form.Section>
+              </Card>
+
+              <Card>
+                <Form.Section text='配置邮箱域名白名单'>
+                  <Text>用以防止恶意用户利用临时邮箱批量注册</Text>
+                  <Row
+                    gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
+                  >
+                    <Col xs={24} sm={24} md={12} lg={12} xl={12}>
+                      <Form.Checkbox
+                        field='EmailDomainRestrictionEnabled'
+                        noLabel
+                        onChange={(e) =>
+                          handleCheckboxChange(
+                            'EmailDomainRestrictionEnabled',
+                            e,
+                          )
+                        }
+                      >
+                        启用邮箱域名白名单
+                      </Form.Checkbox>
+                    </Col>
+                    <Col xs={24} sm={24} md={12} lg={12} xl={12}>
+                      <Form.Checkbox
+                        field='EmailAliasRestrictionEnabled'
+                        noLabel
+                        onChange={(e) =>
+                          handleCheckboxChange(
+                            'EmailAliasRestrictionEnabled',
+                            e,
+                          )
+                        }
+                      >
+                        启用邮箱别名限制
+                      </Form.Checkbox>
+                    </Col>
+                  </Row>
+                  <TagInput
+                    value={emailDomainWhitelist}
+                    onChange={setEmailDomainWhitelist}
+                    placeholder='输入域名后回车'
+                    style={{ width: '100%', marginTop: 16 }}
+                  />
+                  <Button
+                    onClick={submitEmailDomainWhitelist}
+                    style={{ marginTop: 10 }}
+                  >
+                    保存邮箱域名白名单设置
+                  </Button>
+                </Form.Section>
+              </Card>
+              <Card>
+                <Form.Section text='配置 SMTP'>
+                  <Text>用以支持系统的邮件发送</Text>
+                  <Row
+                    gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
+                  >
+                    <Col xs={24} sm={24} md={8} lg={8} xl={8}>
+                      <Form.Input field='SMTPServer' label='SMTP 服务器地址' />
+                    </Col>
+                    <Col xs={24} sm={24} md={8} lg={8} xl={8}>
+                      <Form.Input field='SMTPPort' label='SMTP 端口' />
+                    </Col>
+                    <Col xs={24} sm={24} md={8} lg={8} xl={8}>
+                      <Form.Input field='SMTPAccount' label='SMTP 账户' />
+                    </Col>
+                  </Row>
+                  <Row
+                    gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
+                    style={{ marginTop: 16 }}
+                  >
+                    <Col xs={24} sm={24} md={8} lg={8} xl={8}>
+                      <Form.Input field='SMTPFrom' label='SMTP 发送者邮箱' />
+                    </Col>
+                    <Col xs={24} sm={24} md={8} lg={8} xl={8}>
+                      <Form.Input
+                        field='SMTPToken'
+                        label='SMTP 访问凭证'
+                        type='password'
+                        placeholder='敏感信息不会发送到前端显示'
+                      />
+                    </Col>
+                    <Col xs={24} sm={24} md={8} lg={8} xl={8}>
+                      <Form.Checkbox
+                        field='SMTPSSLEnabled'
+                        noLabel
+                        onChange={(e) =>
+                          handleCheckboxChange('SMTPSSLEnabled', e)
+                        }
+                      >
+                        启用SMTP SSL
+                      </Form.Checkbox>
+                    </Col>
+                  </Row>
+                  <Button onClick={submitSMTP}>保存 SMTP 设置</Button>
+                </Form.Section>
+              </Card>
+              <Card>
+                <Form.Section text='配置 OIDC'>
+                  <Text>
+                    用以支持通过 OIDC 登录,例如 Okta、Auth0 等兼容 OIDC 协议的
+                    IdP
+                  </Text>
+                  <Banner
+                    type='info'
+                    description={`主页链接填 ${inputs.ServerAddress ? inputs.ServerAddress : '网站地址'},重定向 URL 填 ${inputs.ServerAddress ? inputs.ServerAddress : '网站地址'}/oauth/oidc`}
+                    style={{ marginBottom: 20, marginTop: 16 }}
+                  />
+                  <Text>
+                    若你的 OIDC Provider 支持 Discovery Endpoint,你可以仅填写
+                    OIDC Well-Known URL,系统会自动获取 OIDC 配置
+                  </Text>
+                  <Row
+                    gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
+                  >
+                    <Col xs={24} sm={24} md={12} lg={12} xl={12}>
+                      <Form.Input
+                        field="['oidc.well_known']"
+                        label='Well-Known URL'
+                        placeholder='请输入 OIDC 的 Well-Known URL'
+                      />
+                    </Col>
+                    <Col xs={24} sm={24} md={12} lg={12} xl={12}>
+                      <Form.Input
+                        field="['oidc.client_id']"
+                        label='Client ID'
+                        placeholder='输入 OIDC 的 Client ID'
+                      />
+                    </Col>
+                  </Row>
+                  <Row
+                    gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
+                  >
+                    <Col xs={24} sm={24} md={12} lg={12} xl={12}>
+                      <Form.Input
+                        field="['oidc.client_secret']"
+                        label='Client Secret'
+                        type='password'
+                        placeholder='敏感信息不会发送到前端显示'
+                      />
+                    </Col>
+                    <Col xs={24} sm={24} md={12} lg={12} xl={12}>
+                      <Form.Input
+                        field="['oidc.authorization_endpoint']"
+                        label='Authorization Endpoint'
+                        placeholder='输入 OIDC 的 Authorization Endpoint'
+                      />
+                    </Col>
+                  </Row>
+                  <Row
+                    gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
+                  >
+                    <Col xs={24} sm={24} md={12} lg={12} xl={12}>
+                      <Form.Input
+                        field="['oidc.token_endpoint']"
+                        label='Token Endpoint'
+                        placeholder='输入 OIDC 的 Token Endpoint'
+                      />
+                    </Col>
+                    <Col xs={24} sm={24} md={12} lg={12} xl={12}>
+                      <Form.Input
+                        field="['oidc.user_info_endpoint']"
+                        label='User Info Endpoint'
+                        placeholder='输入 OIDC 的 Userinfo Endpoint'
+                      />
+                    </Col>
+                  </Row>
+                  <Button onClick={submitOIDCSettings}>保存 OIDC 设置</Button>
+                </Form.Section>
+              </Card>
+
+              <Card>
+                <Form.Section text='配置 GitHub OAuth App'>
+                  <Text>用以支持通过 GitHub 进行登录注册</Text>
+                  <Banner
+                    type='info'
+                    description={`Homepage URL 填 ${inputs.ServerAddress ? inputs.ServerAddress : '网站地址'},Authorization callback URL 填 ${inputs.ServerAddress ? inputs.ServerAddress : '网站地址'}/oauth/github`}
+                    style={{ marginBottom: 20, marginTop: 16 }}
+                  />
+                  <Row
+                    gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
+                  >
+                    <Col xs={24} sm={24} md={12} lg={12} xl={12}>
+                      <Form.Input
+                        field='GitHubClientId'
+                        label='GitHub Client ID'
+                      />
+                    </Col>
+                    <Col xs={24} sm={24} md={12} lg={12} xl={12}>
+                      <Form.Input
+                        field='GitHubClientSecret'
+                        label='GitHub Client Secret'
+                        type='password'
+                        placeholder='敏感信息不会发送到前端显示'
+                      />
+                    </Col>
+                  </Row>
+                  <Button onClick={submitGitHubOAuth}>
+                    保存 GitHub OAuth 设置
+                  </Button>
+                </Form.Section>
+              </Card>
+              <Card>
+                <Form.Section text='配置 Linux DO OAuth'>
+                  <Text>
+                    用以支持通过 Linux DO 进行登录注册
+                    <a
+                      href='https://connect.linux.do/'
+                      target='_blank'
+                      rel='noreferrer'
+                      style={{
+                        display: 'inline-block',
+                        marginLeft: 4,
+                        marginRight: 4,
+                      }}
                     >
-                      启用SMTP SSL
-                    </Form.Checkbox>
-                  </Col>
-                </Row>
-                <Button onClick={submitSMTP}>保存 SMTP 设置</Button>
-              </Form.Section>
-
-              <Form.Section text='配置 OIDC'>
-                <Text>用以支持通过 OIDC 登录,例如 Okta、Auth0 等兼容 OIDC 协议的 IdP</Text>
-                <Banner
-                  type='info'
-                  description={`主页链接填 ${inputs.ServerAddress ? inputs.ServerAddress : '网站地址'},重定向 URL 填 ${inputs.ServerAddress ? inputs.ServerAddress : '网站地址'}/oauth/oidc`}
-                  style={{ marginBottom: 20, marginTop: 16 }}
-                />
-                <Text>若你的 OIDC Provider 支持 Discovery Endpoint,你可以仅填写 OIDC Well-Known URL,系统会自动获取 OIDC 配置</Text>
-                <Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
-                  <Col xs={24} sm={24} md={12} lg={12} xl={12}>
-                    <Form.Input
-                      field="['oidc.well_known']"
-                      label='Well-Known URL'
-                      placeholder='请输入 OIDC 的 Well-Known URL'
-                    />
-                  </Col>
-                  <Col xs={24} sm={24} md={12} lg={12} xl={12}>
-                    <Form.Input
-                      field="['oidc.client_id']"
-                      label='Client ID'
-                      placeholder='输入 OIDC 的 Client ID'
-                    />
-                  </Col>
-                </Row>
-                <Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
-                  <Col xs={24} sm={24} md={12} lg={12} xl={12}>
-                    <Form.Input
-                      field="['oidc.client_secret']"
-                      label='Client Secret'
-                      type='password'
-                      placeholder='敏感信息不会发送到前端显示'
-                    />
-                  </Col>
-                  <Col xs={24} sm={24} md={12} lg={12} xl={12}>
-                    <Form.Input
-                      field="['oidc.authorization_endpoint']"
-                      label='Authorization Endpoint'
-                      placeholder='输入 OIDC 的 Authorization Endpoint'
-                    />
-                  </Col>
-                </Row>
-                <Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
-                  <Col xs={24} sm={24} md={12} lg={12} xl={12}>
-                    <Form.Input
-                      field="['oidc.token_endpoint']"
-                      label='Token Endpoint'
-                      placeholder='输入 OIDC 的 Token Endpoint'
-                    />
-                  </Col>
-                  <Col xs={24} sm={24} md={12} lg={12} xl={12}>
-                    <Form.Input
-                      field="['oidc.user_info_endpoint']"
-                      label='User Info Endpoint'
-                      placeholder='输入 OIDC 的 Userinfo Endpoint'
-                    />
-                  </Col>
-                </Row>
-                <Button onClick={submitOIDCSettings}>保存 OIDC 设置</Button>
-              </Form.Section>
-
-              <Form.Section text='配置 GitHub OAuth App'>
-                <Text>用以支持通过 GitHub 进行登录注册</Text>
-                <Banner
-                  type='info'
-                  description={`Homepage URL 填 ${inputs.ServerAddress ? inputs.ServerAddress : '网站地址'},Authorization callback URL 填 ${inputs.ServerAddress ? inputs.ServerAddress : '网站地址'}/oauth/github`}
-                  style={{ marginBottom: 20, marginTop: 16 }}
-                />
-                <Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
-                  <Col xs={24} sm={24} md={12} lg={12} xl={12}>
-                    <Form.Input field='GitHubClientId' label='GitHub Client ID' />
-                  </Col>
-                  <Col xs={24} sm={24} md={12} lg={12} xl={12}>
-                    <Form.Input
-                      field='GitHubClientSecret'
-                      label='GitHub Client Secret'
-                      type='password'
-                      placeholder='敏感信息不会发送到前端显示'
-                    />
-                  </Col>
-                </Row>
-                <Button onClick={submitGitHubOAuth}>
-                  保存 GitHub OAuth 设置
-                </Button>
-              </Form.Section>
-              <Form.Section text='配置 Linux DO OAuth'>
-                <Text>
-                  用以支持通过 Linux DO 进行登录注册
-                  <a
-                    href='https://connect.linux.do/'
-                    target='_blank'
-                    rel='noreferrer'
-                    style={{ display: 'inline-block', marginLeft: 4, marginRight: 4 }}
+                      点击此处
+                    </a>
+                    管理你的 LinuxDO OAuth App
+                  </Text>
+                  <Banner
+                    type='info'
+                    description={`回调 URL 填 ${inputs.ServerAddress ? inputs.ServerAddress : '网站地址'}/oauth/linuxdo`}
+                    style={{ marginBottom: 20, marginTop: 16 }}
+                  />
+                  <Row
+                    gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
                   >
-                    点击此处
-                  </a>
-                  管理你的 LinuxDO OAuth App
-                </Text>
-                <Banner
-                  type='info'
-                  description={`回调 URL 填 ${inputs.ServerAddress ? inputs.ServerAddress : '网站地址'}/oauth/linuxdo`}
-                  style={{ marginBottom: 20, marginTop: 16 }}
-                />
-                <Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
-                  <Col xs={24} sm={24} md={12} lg={12} xl={12}>
-                    <Form.Input 
-                      field='LinuxDOClientId' 
-                      label='Linux DO Client ID'
-                      placeholder='输入你注册的 LinuxDO OAuth APP 的 ID'
-                    />
-                  </Col>
-                  <Col xs={24} sm={24} md={12} lg={12} xl={12}>
-                    <Form.Input
-                      field='LinuxDOClientSecret'
-                      label='Linux DO Client Secret'
-                      type='password'
-                      placeholder='敏感信息不会发送到前端显示'
-                    />
-                  </Col>
-                </Row>
-                <Button onClick={submitLinuxDOOAuth}>
-                  保存 Linux DO OAuth 设置
-                </Button>
-              </Form.Section>
-              <Form.Section text='配置 WeChat Server'>
-                <Text>用以支持通过微信进行登录注册</Text>
-                <Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
-                  <Col xs={24} sm={24} md={8} lg={8} xl={8}>
-                    <Form.Input
-                      field='WeChatServerAddress'
-                      label='WeChat Server 服务器地址'
-                    />
-                  </Col>
-                  <Col xs={24} sm={24} md={8} lg={8} xl={8}>
-                    <Form.Input
-                      field='WeChatServerToken'
-                      label='WeChat Server 访问凭证'
-                      type='password'
-                      placeholder='敏感信息不会发送到前端显示'
-                    />
-                  </Col>
-                  <Col xs={24} sm={24} md={8} lg={8} xl={8}>
-                    <Form.Input
-                      field='WeChatAccountQRCodeImageURL'
-                      label='微信公众号二维码图片链接'
-                    />
-                  </Col>
-                </Row>
-                <Button onClick={submitWeChat}>保存 WeChat Server 设置</Button>
-              </Form.Section>
-              <Form.Section text='配置 Telegram 登录'>
-                <Text>用以支持通过 Telegram 进行登录注册</Text>
-                <Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
-                  <Col xs={24} sm={24} md={12} lg={12} xl={12}>
-                    <Form.Input
-                      field='TelegramBotToken'
-                      label='Telegram Bot Token'
-                      placeholder='敏感信息不会发送到前端显示'
-                      type='password'
-                    />
-                  </Col>
-                  <Col xs={24} sm={24} md={12} lg={12} xl={12}>
-                    <Form.Input
-                      field='TelegramBotName'
-                      label='Telegram Bot 名称'
-                    />
-                  </Col>
-                </Row>
-                <Button onClick={submitTelegramSettings}>
-                  保存 Telegram 登录设置
-                </Button>
-              </Form.Section>
-              <Form.Section text='配置 Turnstile'>
-                <Text>用以支持用户校验</Text>
-                <Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
-                  <Col xs={24} sm={24} md={12} lg={12} xl={12}>
-                    <Form.Input
-                      field='TurnstileSiteKey'
-                      label='Turnstile Site Key'
-                    />
-                  </Col>
-                  <Col xs={24} sm={24} md={12} lg={12} xl={12}>
-                    <Form.Input
-                      field='TurnstileSecretKey'
-                      label='Turnstile Secret Key'
-                      type='password'
-                      placeholder='敏感信息不会发送到前端显示'
-                    />
-                  </Col>
-                </Row>
-                <Button onClick={submitTurnstile}>保存 Turnstile 设置</Button>
-              </Form.Section>
-            
+                    <Col xs={24} sm={24} md={12} lg={12} xl={12}>
+                      <Form.Input
+                        field='LinuxDOClientId'
+                        label='Linux DO Client ID'
+                        placeholder='输入你注册的 LinuxDO OAuth APP 的 ID'
+                      />
+                    </Col>
+                    <Col xs={24} sm={24} md={12} lg={12} xl={12}>
+                      <Form.Input
+                        field='LinuxDOClientSecret'
+                        label='Linux DO Client Secret'
+                        type='password'
+                        placeholder='敏感信息不会发送到前端显示'
+                      />
+                    </Col>
+                  </Row>
+                  <Button onClick={submitLinuxDOOAuth}>
+                    保存 Linux DO OAuth 设置
+                  </Button>
+                </Form.Section>
+              </Card>
+
+              <Card>
+                <Form.Section text='配置 WeChat Server'>
+                  <Text>用以支持通过微信进行登录注册</Text>
+                  <Row
+                    gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
+                  >
+                    <Col xs={24} sm={24} md={8} lg={8} xl={8}>
+                      <Form.Input
+                        field='WeChatServerAddress'
+                        label='WeChat Server 服务器地址'
+                      />
+                    </Col>
+                    <Col xs={24} sm={24} md={8} lg={8} xl={8}>
+                      <Form.Input
+                        field='WeChatServerToken'
+                        label='WeChat Server 访问凭证'
+                        type='password'
+                        placeholder='敏感信息不会发送到前端显示'
+                      />
+                    </Col>
+                    <Col xs={24} sm={24} md={8} lg={8} xl={8}>
+                      <Form.Input
+                        field='WeChatAccountQRCodeImageURL'
+                        label='微信公众号二维码图片链接'
+                      />
+                    </Col>
+                  </Row>
+                  <Button onClick={submitWeChat}>
+                    保存 WeChat Server 设置
+                  </Button>
+                </Form.Section>
+              </Card>
+
+              <Card>
+                <Form.Section text='配置 Telegram 登录'>
+                  <Text>用以支持通过 Telegram 进行登录注册</Text>
+                  <Row
+                    gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
+                  >
+                    <Col xs={24} sm={24} md={12} lg={12} xl={12}>
+                      <Form.Input
+                        field='TelegramBotToken'
+                        label='Telegram Bot Token'
+                        placeholder='敏感信息不会发送到前端显示'
+                        type='password'
+                      />
+                    </Col>
+                    <Col xs={24} sm={24} md={12} lg={12} xl={12}>
+                      <Form.Input
+                        field='TelegramBotName'
+                        label='Telegram Bot 名称'
+                      />
+                    </Col>
+                  </Row>
+                  <Button onClick={submitTelegramSettings}>
+                    保存 Telegram 登录设置
+                  </Button>
+                </Form.Section>
+              </Card>
+
+              <Card>
+                <Form.Section text='配置 Turnstile'>
+                  <Text>用以支持用户校验</Text>
+                  <Row
+                    gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
+                  >
+                    <Col xs={24} sm={24} md={12} lg={12} xl={12}>
+                      <Form.Input
+                        field='TurnstileSiteKey'
+                        label='Turnstile Site Key'
+                      />
+                    </Col>
+                    <Col xs={24} sm={24} md={12} lg={12} xl={12}>
+                      <Form.Input
+                        field='TurnstileSecretKey'
+                        label='Turnstile Secret Key'
+                        type='password'
+                        placeholder='敏感信息不会发送到前端显示'
+                      />
+                    </Col>
+                  </Row>
+                  <Button onClick={submitTurnstile}>保存 Turnstile 设置</Button>
+                </Form.Section>
+              </Card>
+
               <Modal
-                title="确认取消密码登录"
+                title='确认取消密码登录'
                 visible={showPasswordLoginConfirmModal}
                 onOk={handlePasswordLoginConfirm}
                 onCancel={() => {
                   setShowPasswordLoginConfirmModal(false);
                   formApiRef.current.setValue('PasswordLoginEnabled', true);
                 }}
-                okText="确认"
-                cancelText="取消"
+                okText='确认'
+                cancelText='取消'
               >
                 <p>您确定要取消密码登录功能吗?这可能会影响用户的登录方式。</p>
               </Modal>
-            </>
+            </div>
           )}
         </Form>
       ) : (
-        <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
-          <Spin size="large" />
+        <div
+          style={{
+            display: 'flex',
+            justifyContent: 'center',
+            alignItems: 'center',
+            height: '100vh',
+          }}
+        >
+          <Spin size='large' />
         </div>
       )}
     </div>
   );
 };
 
-export default SystemSetting;
+export default SystemSetting;

+ 480 - 368
web/src/components/TaskLogsTable.js

@@ -1,400 +1,512 @@
 import React, { useEffect, useState } from 'react';
 import { Label } from 'semantic-ui-react';
-import { API, copy, isAdmin, showError, showSuccess, timestamp2string } from '../helpers';
+import {
+  API,
+  copy,
+  isAdmin,
+  showError,
+  showSuccess,
+  timestamp2string,
+} from '../helpers';
 
 import {
-    Table,
-    Tag,
-    Form,
-    Button,
-    Layout,
-    Modal,
-    Typography, Progress, Card
+  Table,
+  Tag,
+  Form,
+  Button,
+  Layout,
+  Modal,
+  Typography,
+  Progress,
+  Card,
 } from '@douyinfe/semi-ui';
 import { ITEMS_PER_PAGE } from '../constants';
 
-const colors = ['amber', 'blue', 'cyan', 'green', 'grey', 'indigo',
-    'light-blue', 'lime', 'orange', 'pink',
-    'purple', 'red', 'teal', 'violet', 'yellow'
-]
-
+const colors = [
+  'amber',
+  'blue',
+  'cyan',
+  'green',
+  'grey',
+  'indigo',
+  'light-blue',
+  'lime',
+  'orange',
+  'pink',
+  'purple',
+  'red',
+  'teal',
+  'violet',
+  'yellow',
+];
 
 const renderTimestamp = (timestampInSeconds) => {
-    const date = new Date(timestampInSeconds * 1000); // 从秒转换为毫秒
+  const date = new Date(timestampInSeconds * 1000); // 从秒转换为毫秒
 
-    const year = date.getFullYear(); // 获取年份
-    const month = ('0' + (date.getMonth() + 1)).slice(-2); // 获取月份,从0开始需要+1,并保证两位数
-    const day = ('0' + date.getDate()).slice(-2); // 获取日期,并保证两位数
-    const hours = ('0' + date.getHours()).slice(-2); // 获取小时,并保证两位数
-    const minutes = ('0' + date.getMinutes()).slice(-2); // 获取分钟,并保证两位数
-    const seconds = ('0' + date.getSeconds()).slice(-2); // 获取秒钟,并保证两位数
+  const year = date.getFullYear(); // 获取年份
+  const month = ('0' + (date.getMonth() + 1)).slice(-2); // 获取月份,从0开始需要+1,并保证两位数
+  const day = ('0' + date.getDate()).slice(-2); // 获取日期,并保证两位数
+  const hours = ('0' + date.getHours()).slice(-2); // 获取小时,并保证两位数
+  const minutes = ('0' + date.getMinutes()).slice(-2); // 获取分钟,并保证两位数
+  const seconds = ('0' + date.getSeconds()).slice(-2); // 获取秒钟,并保证两位数
 
-    return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; // 格式化输出
+  return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; // 格式化输出
 };
 
 function renderDuration(submit_time, finishTime) {
-    // 确保startTime和finishTime都是有效的时间戳
-    if (!submit_time || !finishTime) return 'N/A';
+  // 确保startTime和finishTime都是有效的时间戳
+  if (!submit_time || !finishTime) return 'N/A';
 
-    // 将时间戳转换为Date对象
-    const start = new Date(submit_time);
-    const finish = new Date(finishTime);
+  // 将时间戳转换为Date对象
+  const start = new Date(submit_time);
+  const finish = new Date(finishTime);
 
-    // 计算时间差(毫秒)
-    const durationMs = finish - start;
+  // 计算时间差(毫秒)
+  const durationMs = finish - start;
 
-    // 将时间差转换为秒,并保留一位小数
-    const durationSec = (durationMs / 1000).toFixed(1);
+  // 将时间差转换为秒,并保留一位小数
+  const durationSec = (durationMs / 1000).toFixed(1);
 
-    // 设置颜色:大于60秒则为红色,小于等于60秒则为绿色
-    const color = durationSec > 60 ? 'red' : 'green';
+  // 设置颜色:大于60秒则为红色,小于等于60秒则为绿色
+  const color = durationSec > 60 ? 'red' : 'green';
 
-    // 返回带有样式的颜色标签
-    return (
-        <Tag color={color} size="large">
-            {durationSec} 秒
-        </Tag>
-    );
+  // 返回带有样式的颜色标签
+  return (
+    <Tag color={color} size='large'>
+      {durationSec} 秒
+    </Tag>
+  );
 }
 
 const LogsTable = () => {
-    const [isModalOpen, setIsModalOpen] = useState(false);
-    const [modalContent, setModalContent] = useState('');
-    const isAdminUser = isAdmin();
-    const columns = [
-        {
-            title: "提交时间",
-            dataIndex: 'submit_time',
-            render: (text, record, index) => {
-                return (
-                    <div>
-                        {text ? renderTimestamp(text) : "-"}
-                    </div>
-                );
-            },
-        },
-        {
-            title: "结束时间",
-            dataIndex: 'finish_time',
-            render: (text, record, index) => {
-                return (
-                    <div>
-                        {text ? renderTimestamp(text) : "-"}
-                    </div>
-                );
-            },
-        },
-        {
-            title: '进度',
-            dataIndex: 'progress',
-            width: 50,
-            render: (text, record, index) => {
-                return (
-                    <div>
-                        {
-                            // 转换例如100%为数字100,如果text未定义,返回0
-                            isNaN(text.replace('%', '')) ? text : <Progress width={42} type="circle" showInfo={true} percent={Number(text.replace('%', '') || 0)} aria-label="drawing progress" />
-                        }
-                    </div>
-                );
-            },
-        },
-        {
-            title: '花费时间',
-            dataIndex: 'finish_time', // 以finish_time作为dataIndex
-            key: 'finish_time',
-            render: (finish, record) => {
-                // 假设record.start_time是存在的,并且finish是完成时间的时间戳
-                return <>
-                    {
-                        finish ? renderDuration(record.submit_time, finish) : "-"
-                    }
-                </>
-            },
-        },
-        {
-            title: "渠道",
-            dataIndex: 'channel_id',
-            className: isAdminUser ? 'tableShow' : 'tableHiddle',
-            render: (text, record, index) => {
-                return (
-                    <div>
-                        <Tag
-                            color={colors[parseInt(text) % colors.length]}
-                            size='large'
-                            onClick={() => {
-                                copyText(text); // 假设copyText是用于文本复制的函数
-                            }}
-                        >
-                            {' '}
-                            {text}{' '}
-                        </Tag>
-                    </div>
-                );
-            },
-        },
-        {
-            title: "平台",
-            dataIndex: 'platform',
-            render: (text, record, index) => {
-                return (
-                    <div>
-                        {renderPlatform(text)}
-                    </div>
-                );
-            },
-        },
-        {
-            title: '类型',
-            dataIndex: 'action',
-            render: (text, record, index) => {
-                return (
-                    <div>
-                        {renderType(text)}
-                    </div>
-                );
-            },
-        },
-        {
-            title: '任务ID(点击查看详情)',
-            dataIndex: 'task_id',
-            render: (text, record, index) => {
-                return (<Typography.Text
-                    ellipsis={{ showTooltip: true }}
-                    //style={{width: 100}}
-                    onClick={() => {
-                        setModalContent(JSON.stringify(record, null, 2));
-                        setIsModalOpen(true);
-                    }}
-                >
-                    <div>
-                        {text}
-                    </div>
-                </Typography.Text>);
-            },
-        },
-        {
-            title: '任务状态',
-            dataIndex: 'status',
-            render: (text, record, index) => {
-                return (
-                    <div>
-                        {renderStatus(text)}
-                    </div>
-                );
-            },
-        },
-
-        {
-            title: '失败原因',
-            dataIndex: 'fail_reason',
-            render: (text, record, index) => {
-                // 如果text未定义,返回替代文本,例如空字符串''或其他
-                if (!text) {
-                    return '无';
-                }
-
-                return (
-                    <Typography.Text
-                        ellipsis={{ showTooltip: true }}
-                        style={{ width: 100 }}
-                        onClick={() => {
-                            setModalContent(text);
-                            setIsModalOpen(true);
-                        }}
-                    >
-                        {text}
-                    </Typography.Text>
-                );
+  const [isModalOpen, setIsModalOpen] = useState(false);
+  const [modalContent, setModalContent] = useState('');
+  const isAdminUser = isAdmin();
+  const columns = [
+    {
+      title: '提交时间',
+      dataIndex: 'submit_time',
+      render: (text, record, index) => {
+        return <div>{text ? renderTimestamp(text) : '-'}</div>;
+      },
+    },
+    {
+      title: '结束时间',
+      dataIndex: 'finish_time',
+      render: (text, record, index) => {
+        return <div>{text ? renderTimestamp(text) : '-'}</div>;
+      },
+    },
+    {
+      title: '进度',
+      dataIndex: 'progress',
+      width: 50,
+      render: (text, record, index) => {
+        return (
+          <div>
+            {
+              // 转换例如100%为数字100,如果text未定义,返回0
+              isNaN(text.replace('%', '')) ? (
+                text
+              ) : (
+                <Progress
+                  width={42}
+                  type='circle'
+                  showInfo={true}
+                  percent={Number(text.replace('%', '') || 0)}
+                  aria-label='drawing progress'
+                />
+              )
             }
+          </div>
+        );
+      },
+    },
+    {
+      title: '花费时间',
+      dataIndex: 'finish_time', // 以finish_time作为dataIndex
+      key: 'finish_time',
+      render: (finish, record) => {
+        // 假设record.start_time是存在的,并且finish是完成时间的时间戳
+        return <>{finish ? renderDuration(record.submit_time, finish) : '-'}</>;
+      },
+    },
+    {
+      title: '渠道',
+      dataIndex: 'channel_id',
+      className: isAdminUser ? 'tableShow' : 'tableHiddle',
+      render: (text, record, index) => {
+        return (
+          <div>
+            <Tag
+              color={colors[parseInt(text) % colors.length]}
+              size='large'
+              onClick={() => {
+                copyText(text); // 假设copyText是用于文本复制的函数
+              }}
+            >
+              {' '}
+              {text}{' '}
+            </Tag>
+          </div>
+        );
+      },
+    },
+    {
+      title: '平台',
+      dataIndex: 'platform',
+      render: (text, record, index) => {
+        return <div>{renderPlatform(text)}</div>;
+      },
+    },
+    {
+      title: '类型',
+      dataIndex: 'action',
+      render: (text, record, index) => {
+        return <div>{renderType(text)}</div>;
+      },
+    },
+    {
+      title: '任务ID(点击查看详情)',
+      dataIndex: 'task_id',
+      render: (text, record, index) => {
+        return (
+          <Typography.Text
+            ellipsis={{ showTooltip: true }}
+            //style={{width: 100}}
+            onClick={() => {
+              setModalContent(JSON.stringify(record, null, 2));
+              setIsModalOpen(true);
+            }}
+          >
+            <div>{text}</div>
+          </Typography.Text>
+        );
+      },
+    },
+    {
+      title: '任务状态',
+      dataIndex: 'status',
+      render: (text, record, index) => {
+        return <div>{renderStatus(text)}</div>;
+      },
+    },
+
+    {
+      title: '失败原因',
+      dataIndex: 'fail_reason',
+      render: (text, record, index) => {
+        // 如果text未定义,返回替代文本,例如空字符串''或其他
+        if (!text) {
+          return '无';
         }
-    ];
-
-    const [logs, setLogs] = useState([]);
-    const [loading, setLoading] = useState(true);
-    const [activePage, setActivePage] = useState(1);
-    const [logCount, setLogCount] = useState(ITEMS_PER_PAGE);
-    const [logType] = useState(0);
-
-    let now = new Date();
-    // 初始化start_timestamp为前一天
-    let zeroNow = new Date(now.getFullYear(), now.getMonth(), now.getDate());
-    const [inputs, setInputs] = useState({
-        channel_id: '',
-        task_id: '',
-        start_timestamp: timestamp2string(zeroNow.getTime() /1000),
-        end_timestamp: '',
-    });
-    const { channel_id, task_id, start_timestamp, end_timestamp } = inputs;
-
-    const handleInputChange = (value, name) => {
-        setInputs((inputs) => ({ ...inputs, [name]: value }));
-    };
-
-
-    const setLogsFormat = (logs) => {
-        for (let i = 0; i < logs.length; i++) {
-            logs[i].timestamp2string = timestamp2string(logs[i].created_at);
-            logs[i].key = '' + logs[i].id;
-        }
-        // data.key = '' + data.id
-        setLogs(logs);
-        setLogCount(logs.length + ITEMS_PER_PAGE);
-        // console.log(logCount);
-    }
 
-    const loadLogs = async (startIdx) => {
-        setLoading(true);
-
-        let url = '';
-        let localStartTimestamp = parseInt(Date.parse(start_timestamp) / 1000);
-        let localEndTimestamp = parseInt(Date.parse(end_timestamp) / 1000 );
-        if (isAdminUser) {
-            url = `/api/task/?p=${startIdx}&channel_id=${channel_id}&task_id=${task_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`;
-        } else {
-            url = `/api/task/self?p=${startIdx}&task_id=${task_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`;
-        }
-        const res = await API.get(url);
-        let { success, message, data } = res.data;
-        if (success) {
-            if (startIdx === 0) {
-                setLogsFormat(data);
-            } else {
-                let newLogs = [...logs];
-                newLogs.splice(startIdx * ITEMS_PER_PAGE, data.length, ...data);
-                setLogsFormat(newLogs);
-            }
-        } else {
-            showError(message);
-        }
-        setLoading(false);
-    };
-
-    const pageData = logs.slice((activePage - 1) * ITEMS_PER_PAGE, activePage * ITEMS_PER_PAGE);
-
-    const handlePageChange = page => {
-        setActivePage(page);
-        if (page === Math.ceil(logs.length / ITEMS_PER_PAGE) + 1) {
-            // In this case we have to load more data and then append them.
-            loadLogs(page - 1).then(r => {
-            });
-        }
-    };
-
-    const refresh = async () => {
-        // setLoading(true);
-        setActivePage(1);
-        await loadLogs(0);
-    };
-
-    const copyText = async (text) => {
-        if (await copy(text)) {
-            showSuccess('已复制:' + text);
-        } else {
-            // setSearchKeyword(text);
-            Modal.error({ title: "无法复制到剪贴板,请手动复制", content: text });
-        }
+        return (
+          <Typography.Text
+            ellipsis={{ showTooltip: true }}
+            style={{ width: 100 }}
+            onClick={() => {
+              setModalContent(text);
+              setIsModalOpen(true);
+            }}
+          >
+            {text}
+          </Typography.Text>
+        );
+      },
+    },
+  ];
+
+  const [logs, setLogs] = useState([]);
+  const [loading, setLoading] = useState(true);
+  const [activePage, setActivePage] = useState(1);
+  const [logCount, setLogCount] = useState(ITEMS_PER_PAGE);
+  const [logType] = useState(0);
+
+  let now = new Date();
+  // 初始化start_timestamp为前一天
+  let zeroNow = new Date(now.getFullYear(), now.getMonth(), now.getDate());
+  const [inputs, setInputs] = useState({
+    channel_id: '',
+    task_id: '',
+    start_timestamp: timestamp2string(zeroNow.getTime() / 1000),
+    end_timestamp: '',
+  });
+  const { channel_id, task_id, start_timestamp, end_timestamp } = inputs;
+
+  const handleInputChange = (value, name) => {
+    setInputs((inputs) => ({ ...inputs, [name]: value }));
+  };
+
+  const setLogsFormat = (logs) => {
+    for (let i = 0; i < logs.length; i++) {
+      logs[i].timestamp2string = timestamp2string(logs[i].created_at);
+      logs[i].key = '' + logs[i].id;
     }
-
-    useEffect(() => {
-        refresh().then();
-    }, [logType]);
-
-    const renderType = (type) => {
-        switch (type) {
-            case 'MUSIC':
-                return <Label basic color='grey'> 生成音乐 </Label>;
-            case 'LYRICS':
-                return <Label basic color='pink'> 生成歌词 </Label>;
-
-            default:
-                return <Label basic color='black'> 未知 </Label>;
-        }
+    // data.key = '' + data.id
+    setLogs(logs);
+    setLogCount(logs.length + ITEMS_PER_PAGE);
+    // console.log(logCount);
+  };
+
+  const loadLogs = async (startIdx) => {
+    setLoading(true);
+
+    let url = '';
+    let localStartTimestamp = parseInt(Date.parse(start_timestamp) / 1000);
+    let localEndTimestamp = parseInt(Date.parse(end_timestamp) / 1000);
+    if (isAdminUser) {
+      url = `/api/task/?p=${startIdx}&channel_id=${channel_id}&task_id=${task_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`;
+    } else {
+      url = `/api/task/self?p=${startIdx}&task_id=${task_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`;
     }
-
-    const renderPlatform = (type) => {
-        switch (type) {
-            case "suno":
-                return <Label basic color='green'> Suno </Label>;
-            default:
-                return <Label basic color='black'> 未知 </Label>;
-        }
+    const res = await API.get(url);
+    let { success, message, data } = res.data;
+    if (success) {
+      if (startIdx === 0) {
+        setLogsFormat(data);
+      } else {
+        let newLogs = [...logs];
+        newLogs.splice(startIdx * ITEMS_PER_PAGE, data.length, ...data);
+        setLogsFormat(newLogs);
+      }
+    } else {
+      showError(message);
     }
-
-    const renderStatus = (type) => {
-        switch (type) {
-            case 'SUCCESS':
-                return <Label basic color='green'> 成功 </Label>;
-            case 'NOT_START':
-                return <Label basic color='black'> 未启动 </Label>;
-            case 'SUBMITTED':
-                return <Label basic color='yellow'> 队列中 </Label>;
-            case 'IN_PROGRESS':
-                return <Label basic color='blue'> 执行中 </Label>;
-            case 'FAILURE':
-                return <Label basic color='red'> 失败 </Label>;
-            case 'QUEUED':
-                return <Label basic color='red'> 排队中 </Label>;
-            case 'UNKNOWN':
-                return <Label basic color='red'> 未知 </Label>;
-            case '':
-                return <Label basic color='black'> 正在提交 </Label>;
-            default:
-                return <Label basic color='black'> 未知 </Label>;
-        }
+    setLoading(false);
+  };
+
+  const pageData = logs.slice(
+    (activePage - 1) * ITEMS_PER_PAGE,
+    activePage * ITEMS_PER_PAGE,
+  );
+
+  const handlePageChange = (page) => {
+    setActivePage(page);
+    if (page === Math.ceil(logs.length / ITEMS_PER_PAGE) + 1) {
+      // In this case we have to load more data and then append them.
+      loadLogs(page - 1).then((r) => {});
     }
-
-    return (
-        <>
-
-            <Layout>
-                <Form layout='horizontal' labelPosition='inset'>
-                    <>
-                        {isAdminUser && <Form.Input field="channel_id" label='渠道 ID' style={{ width: '236px', marginBottom: '10px' }} value={channel_id}
-                                                    placeholder={'可选值'} name='channel_id'
-                                                    onChange={value => handleInputChange(value, 'channel_id')} />
-                        }
-                        <Form.Input field="task_id" label={"任务 ID"} style={{ width: '236px', marginBottom: '10px' }} value={task_id}
-                            placeholder={"可选值"}
-                            name='task_id'
-                            onChange={value => handleInputChange(value, 'task_id')} />
-
-                        <Form.DatePicker field="start_timestamp" label={"起始时间"} style={{ width: '236px', marginBottom: '10px' }}
-                            initValue={start_timestamp}
-                            value={start_timestamp} type='dateTime'
-                            name='start_timestamp'
-                            onChange={value => handleInputChange(value, 'start_timestamp')} />
-                        <Form.DatePicker field="end_timestamp" fluid label={"结束时间"} style={{ width: '236px', marginBottom: '10px' }}
-                            initValue={end_timestamp}
-                            value={end_timestamp} type='dateTime'
-                            name='end_timestamp'
-                            onChange={value => handleInputChange(value, 'end_timestamp')} />
-                        <Button label={"查询"} type="primary" htmlType="submit" className="btn-margin-right"
-                            onClick={refresh}>查询</Button>
-                    </>
-                </Form>
-                <Card>
-                    <Table columns={columns} dataSource={pageData} pagination={{
-                        currentPage: activePage,
-                        pageSize: ITEMS_PER_PAGE,
-                        total: logCount,
-                        pageSizeOpts: [10, 20, 50, 100],
-                        onPageChange: handlePageChange,
-                    }} loading={loading} />
-                </Card>
-                <Modal
-                    visible={isModalOpen}
-                    onOk={() => setIsModalOpen(false)}
-                    onCancel={() => setIsModalOpen(false)}
-                    closable={null}
-                    bodyStyle={{ height: '400px', overflow: 'auto' }} // 设置模态框内容区域样式
-                    width={800} // 设置模态框宽度
-                >
-                    <p style={{ whiteSpace: 'pre-line' }}>{modalContent}</p>
-                </Modal>
-            </Layout>
-        </>
-    );
+  };
+
+  const refresh = async () => {
+    // setLoading(true);
+    setActivePage(1);
+    await loadLogs(0);
+  };
+
+  const copyText = async (text) => {
+    if (await copy(text)) {
+      showSuccess('已复制:' + text);
+    } else {
+      // setSearchKeyword(text);
+      Modal.error({ title: '无法复制到剪贴板,请手动复制', content: text });
+    }
+  };
+
+  useEffect(() => {
+    refresh().then();
+  }, [logType]);
+
+  const renderType = (type) => {
+    switch (type) {
+      case 'MUSIC':
+        return (
+          <Label basic color='grey'>
+            {' '}
+            生成音乐{' '}
+          </Label>
+        );
+      case 'LYRICS':
+        return (
+          <Label basic color='pink'>
+            {' '}
+            生成歌词{' '}
+          </Label>
+        );
+
+      default:
+        return (
+          <Label basic color='black'>
+            {' '}
+            未知{' '}
+          </Label>
+        );
+    }
+  };
+
+  const renderPlatform = (type) => {
+    switch (type) {
+      case 'suno':
+        return (
+          <Label basic color='green'>
+            {' '}
+            Suno{' '}
+          </Label>
+        );
+      default:
+        return (
+          <Label basic color='black'>
+            {' '}
+            未知{' '}
+          </Label>
+        );
+    }
+  };
+
+  const renderStatus = (type) => {
+    switch (type) {
+      case 'SUCCESS':
+        return (
+          <Label basic color='green'>
+            {' '}
+            成功{' '}
+          </Label>
+        );
+      case 'NOT_START':
+        return (
+          <Label basic color='black'>
+            {' '}
+            未启动{' '}
+          </Label>
+        );
+      case 'SUBMITTED':
+        return (
+          <Label basic color='yellow'>
+            {' '}
+            队列中{' '}
+          </Label>
+        );
+      case 'IN_PROGRESS':
+        return (
+          <Label basic color='blue'>
+            {' '}
+            执行中{' '}
+          </Label>
+        );
+      case 'FAILURE':
+        return (
+          <Label basic color='red'>
+            {' '}
+            失败{' '}
+          </Label>
+        );
+      case 'QUEUED':
+        return (
+          <Label basic color='red'>
+            {' '}
+            排队中{' '}
+          </Label>
+        );
+      case 'UNKNOWN':
+        return (
+          <Label basic color='red'>
+            {' '}
+            未知{' '}
+          </Label>
+        );
+      case '':
+        return (
+          <Label basic color='black'>
+            {' '}
+            正在提交{' '}
+          </Label>
+        );
+      default:
+        return (
+          <Label basic color='black'>
+            {' '}
+            未知{' '}
+          </Label>
+        );
+    }
+  };
+
+  return (
+    <>
+      <Layout>
+        <Form layout='horizontal' labelPosition='inset'>
+          <>
+            {isAdminUser && (
+              <Form.Input
+                field='channel_id'
+                label='渠道 ID'
+                style={{ width: '236px', marginBottom: '10px' }}
+                value={channel_id}
+                placeholder={'可选值'}
+                name='channel_id'
+                onChange={(value) => handleInputChange(value, 'channel_id')}
+              />
+            )}
+            <Form.Input
+              field='task_id'
+              label={'任务 ID'}
+              style={{ width: '236px', marginBottom: '10px' }}
+              value={task_id}
+              placeholder={'可选值'}
+              name='task_id'
+              onChange={(value) => handleInputChange(value, 'task_id')}
+            />
+
+            <Form.DatePicker
+              field='start_timestamp'
+              label={'起始时间'}
+              style={{ width: '236px', marginBottom: '10px' }}
+              initValue={start_timestamp}
+              value={start_timestamp}
+              type='dateTime'
+              name='start_timestamp'
+              onChange={(value) => handleInputChange(value, 'start_timestamp')}
+            />
+            <Form.DatePicker
+              field='end_timestamp'
+              fluid
+              label={'结束时间'}
+              style={{ width: '236px', marginBottom: '10px' }}
+              initValue={end_timestamp}
+              value={end_timestamp}
+              type='dateTime'
+              name='end_timestamp'
+              onChange={(value) => handleInputChange(value, 'end_timestamp')}
+            />
+            <Button
+              label={'查询'}
+              type='primary'
+              htmlType='submit'
+              className='btn-margin-right'
+              onClick={refresh}
+            >
+              查询
+            </Button>
+          </>
+        </Form>
+        <Card>
+          <Table
+            columns={columns}
+            dataSource={pageData}
+            pagination={{
+              currentPage: activePage,
+              pageSize: ITEMS_PER_PAGE,
+              total: logCount,
+              pageSizeOpts: [10, 20, 50, 100],
+              onPageChange: handlePageChange,
+            }}
+            loading={loading}
+          />
+        </Card>
+        <Modal
+          visible={isModalOpen}
+          onOk={() => setIsModalOpen(false)}
+          onCancel={() => setIsModalOpen(false)}
+          closable={null}
+          bodyStyle={{ height: '400px', overflow: 'auto' }} // 设置模态框内容区域样式
+          width={800} // 设置模态框宽度
+        >
+          <p style={{ whiteSpace: 'pre-line' }}>{modalContent}</p>
+        </Modal>
+      </Layout>
+    </>
+  );
 };
 
 export default LogsTable;

+ 47 - 41
web/src/components/TokensTable.js

@@ -8,14 +8,16 @@ import {
 } from '../helpers';
 
 import { ITEMS_PER_PAGE } from '../constants';
-import {renderGroup, renderQuota} from '../helpers/render';
+import { renderGroup, renderQuota } from '../helpers/render';
 import {
-  Button, Divider,
+  Button,
+  Divider,
   Dropdown,
   Form,
   Modal,
   Popconfirm,
-  Popover, Space,
+  Popover,
+  Space,
   SplitButtonGroup,
   Table,
   Tag,
@@ -30,7 +32,6 @@ function renderTimestamp(timestamp) {
 }
 
 const TokensTable = () => {
-
   const { t } = useTranslation();
 
   const renderStatus = (status, model_limits_enabled = false) => {
@@ -86,12 +87,14 @@ const TokensTable = () => {
       dataIndex: 'status',
       key: 'status',
       render: (text, record, index) => {
-        return <div>
-          <Space>
-            {renderStatus(text, record.model_limits_enabled)}
-            {renderGroup(record.group)}
-          </Space>
-        </div>;
+        return (
+          <div>
+            <Space>
+              {renderStatus(text, record.model_limits_enabled)}
+              {renderGroup(record.group)}
+            </Space>
+          </div>
+        );
       },
     },
     {
@@ -143,7 +146,7 @@ const TokensTable = () => {
       dataIndex: 'operate',
       render: (text, record, index) => {
         let chats = localStorage.getItem('chats');
-        let chatsArray = []
+        let chatsArray = [];
         let shouldUseCustom = true;
 
         if (shouldUseCustom) {
@@ -153,7 +156,7 @@ const TokensTable = () => {
             // check chats is array
             if (Array.isArray(chats)) {
               for (let i = 0; i < chats.length; i++) {
-                let chat = {}
+                let chat = {};
                 chat.node = 'item';
                 // c is a map
                 // chat.key = chats[i].name;
@@ -164,13 +167,12 @@ const TokensTable = () => {
                     chat.name = key;
                     chat.onClick = () => {
                       onOpenLink(key, chats[i][key], record);
-                    }
+                    };
                   }
                 }
                 chatsArray.push(chat);
               }
             }
-
           } catch (e) {
             console.log(e);
             showError(t('聊天链接配置错误,请联系管理员'));
@@ -208,7 +210,11 @@ const TokensTable = () => {
                   if (chatsArray.length === 0) {
                     showError(t('请联系管理员配置聊天链接'));
                   } else {
-                    onOpenLink('default', chats[0][Object.keys(chats[0])[0]], record);
+                    onOpenLink(
+                      'default',
+                      chats[0][Object.keys(chats[0])[0]],
+                      record,
+                    );
                   }
                 }}
               >
@@ -539,36 +545,36 @@ const TokensTable = () => {
           {t('查询')}
         </Button>
       </Form>
-      <Divider style={{margin:'15px 0'}}/>
+      <Divider style={{ margin: '15px 0' }} />
       <div>
         <Button
-            theme='light'
-            type='primary'
-            style={{ marginRight: 8 }}
-            onClick={() => {
-              setEditingToken({
-                id: undefined,
-              });
-              setShowEdit(true);
-            }}
+          theme='light'
+          type='primary'
+          style={{ marginRight: 8 }}
+          onClick={() => {
+            setEditingToken({
+              id: undefined,
+            });
+            setShowEdit(true);
+          }}
         >
-            {t('添加令牌')}
+          {t('添加令牌')}
         </Button>
         <Button
-            label={t('复制所选令牌')}
-            type='warning'
-            onClick={async () => {
-              if (selectedKeys.length === 0) {
-                showError(t('请至少选择一个令牌!'));
-                return;
-              }
-              let keys = '';
-              for (let i = 0; i < selectedKeys.length; i++) {
-                keys +=
-                    selectedKeys[i].name + '    sk-' + selectedKeys[i].key + '\n';
-              }
-              await copyText(keys);
-            }}
+          label={t('复制所选令牌')}
+          type='warning'
+          onClick={async () => {
+            if (selectedKeys.length === 0) {
+              showError(t('请至少选择一个令牌!'));
+              return;
+            }
+            let keys = '';
+            for (let i = 0; i < selectedKeys.length; i++) {
+              keys +=
+                selectedKeys[i].name + '    sk-' + selectedKeys[i].key + '\n';
+            }
+            await copyText(keys);
+          }}
         >
           {t('复制所选令牌到剪贴板')}
         </Button>
@@ -588,7 +594,7 @@ const TokensTable = () => {
             t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
               start: page.currentStart,
               end: page.currentEnd,
-              total: tokens.length
+              total: tokens.length,
             }),
           onPageSizeChange: (size) => {
             setPageSize(size);

+ 31 - 19
web/src/components/UsersTable.js

@@ -167,7 +167,11 @@ const UsersTable = () => {
                   manageUser(record.id, 'demote', record);
                 }}
               >
-                <Button theme='light' type='secondary' style={{ marginRight: 1 }}>
+                <Button
+                  theme='light'
+                  type='secondary'
+                  style={{ marginRight: 1 }}
+                >
                   {t('降级')}
                 </Button>
               </Popconfirm>
@@ -261,7 +265,7 @@ const UsersTable = () => {
       users[i].key = users[i].id;
     }
     setUsers(users);
-  }
+  };
 
   const loadUsers = async (startIdx, pageSize) => {
     const res = await API.get(`/api/user/?p=${startIdx}&page_size=${pageSize}`);
@@ -277,7 +281,6 @@ const UsersTable = () => {
     setLoading(false);
   };
 
-
   useEffect(() => {
     loadUsers(0, pageSize)
       .then()
@@ -327,22 +330,29 @@ const UsersTable = () => {
     }
   };
 
-  const searchUsers = async (startIdx, pageSize, searchKeyword, searchGroup) => {
+  const searchUsers = async (
+    startIdx,
+    pageSize,
+    searchKeyword,
+    searchGroup,
+  ) => {
     if (searchKeyword === '' && searchGroup === '') {
-        // if keyword is blank, load files instead.
-        await loadUsers(startIdx, pageSize);
-        return;
+      // if keyword is blank, load files instead.
+      await loadUsers(startIdx, pageSize);
+      return;
     }
     setSearching(true);
-    const res = await API.get(`/api/user/search?keyword=${searchKeyword}&group=${searchGroup}&p=${startIdx}&page_size=${pageSize}`);
+    const res = await API.get(
+      `/api/user/search?keyword=${searchKeyword}&group=${searchGroup}&p=${startIdx}&page_size=${pageSize}`,
+    );
     const { success, message, data } = res.data;
     if (success) {
-        const newPageData = data.items;
-        setActivePage(data.page);
-        setUserCount(data.total);
-        setUserFormat(newPageData);
+      const newPageData = data.items;
+      setActivePage(data.page);
+      setUserCount(data.total);
+      setUserFormat(newPageData);
     } else {
-        showError(message);
+      showError(message);
     }
     setSearching(false);
   };
@@ -354,9 +364,9 @@ const UsersTable = () => {
   const handlePageChange = (page) => {
     setActivePage(page);
     if (searchKeyword === '' && searchGroup === '') {
-        loadUsers(page, pageSize).then();
+      loadUsers(page, pageSize).then();
     } else {
-        searchUsers(page, pageSize, searchKeyword, searchGroup).then();
+      searchUsers(page, pageSize, searchKeyword, searchGroup).then();
     }
   };
 
@@ -372,7 +382,7 @@ const UsersTable = () => {
   };
 
   const refresh = async () => {
-    setActivePage(1)
+    setActivePage(1);
     if (searchKeyword === '') {
       await loadUsers(activePage, pageSize);
     } else {
@@ -431,7 +441,9 @@ const UsersTable = () => {
       >
         <div style={{ display: 'flex' }}>
           <Space>
-            <Tooltip content={t('支持搜索用户的 ID、用户名、显示名称和邮箱地址')}>
+            <Tooltip
+              content={t('支持搜索用户的 ID、用户名、显示名称和邮箱地址')}
+            >
               <Form.Input
                 label={t('搜索关键字')}
                 icon='search'
@@ -443,7 +455,7 @@ const UsersTable = () => {
                 onChange={(value) => handleKeywordChange(value)}
               />
             </Tooltip>
-            
+
             <Form.Select
               field='group'
               label={t('分组')}
@@ -482,7 +494,7 @@ const UsersTable = () => {
             t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
               start: page.currentStart,
               end: page.currentEnd,
-              total: users.length
+              total: users.length,
             }),
           currentPage: activePage,
           pageSize: pageSize,

+ 11 - 4
web/src/components/custom/TextInput.js

@@ -1,7 +1,14 @@
 import { Input, Typography } from '@douyinfe/semi-ui';
 import React from 'react';
 
-const TextInput = ({ label, name, value, onChange, placeholder, type = 'text' }) => {
+const TextInput = ({
+  label,
+  name,
+  value,
+  onChange,
+  placeholder,
+  type = 'text',
+}) => {
   return (
     <>
       <div style={{ marginTop: 10 }}>
@@ -12,10 +19,10 @@ const TextInput = ({ label, name, value, onChange, placeholder, type = 'text' })
         placeholder={placeholder}
         onChange={(value) => onChange(value)}
         value={value}
-        autoComplete="new-password"
+        autoComplete='new-password'
       />
     </>
   );
-}
+};
 
-export default TextInput;
+export default TextInput;

+ 3 - 3
web/src/components/custom/TextNumberInput.js

@@ -12,10 +12,10 @@ const TextNumberInput = ({ label, name, value, onChange, placeholder }) => {
         placeholder={placeholder}
         onChange={(value) => onChange(value)}
         value={value}
-        autoComplete="new-password"
+        autoComplete='new-password'
       />
     </>
   );
-}
+};
 
-export default TextNumberInput;
+export default TextNumberInput;

+ 3 - 3
web/src/components/fetchTokenKeys.js

@@ -13,7 +13,7 @@ async function fetchTokenKeys() {
       throw new Error('Failed to fetch token keys');
     }
   } catch (error) {
-    console.error("Error fetching token keys:", error);
+    console.error('Error fetching token keys:', error);
     return [];
   }
 }
@@ -27,7 +27,7 @@ function getServerAddress() {
       status = JSON.parse(status);
       serverAddress = status.server_address || '';
     } catch (error) {
-      console.error("Failed to parse status from localStorage:", error);
+      console.error('Failed to parse status from localStorage:', error);
     }
   }
 
@@ -65,4 +65,4 @@ export function useTokenKeys(id) {
   }, []);
 
   return { keys, serverAddress, isLoading };
-}
+}

+ 3 - 4
web/src/components/utils.js

@@ -20,13 +20,12 @@ export async function onOIDCClicked(auth_url, client_id, openInNewTab = false) {
   const state = await getOAuthState();
   if (!state) return;
   const redirect_uri = `${window.location.origin}/oauth/oidc`;
-  const response_type = "code";
-  const scope = "openid profile email";
+  const response_type = 'code';
+  const scope = 'openid profile email';
   const url = `${auth_url}?client_id=${client_id}&redirect_uri=${redirect_uri}&response_type=${response_type}&scope=${scope}&state=${state}`;
   if (openInNewTab) {
     window.open(url);
-  } else
-  {
+  } else {
     window.location.href = url;
   }
 }

+ 20 - 20
web/src/constants/channel.constants.js

@@ -3,86 +3,86 @@ export const CHANNEL_OPTIONS = [
   {
     value: 2,
     color: 'light-blue',
-    label: 'Midjourney Proxy'
+    label: 'Midjourney Proxy',
   },
   {
     value: 5,
     color: 'blue',
-    label: 'Midjourney Proxy Plus'
+    label: 'Midjourney Proxy Plus',
   },
   {
     value: 36,
     color: 'purple',
-    label: 'Suno API'
+    label: 'Suno API',
   },
   { value: 4, color: 'grey', label: 'Ollama' },
   {
     value: 14,
     color: 'indigo',
-    label: 'Anthropic Claude'
+    label: 'Anthropic Claude',
   },
   {
     value: 33,
     color: 'indigo',
-    label: 'AWS Claude'
+    label: 'AWS Claude',
   },
   { value: 41, color: 'blue', label: 'Vertex AI' },
   {
     value: 3,
     color: 'teal',
-    label: 'Azure OpenAI'
+    label: 'Azure OpenAI',
   },
   {
     value: 34,
     color: 'purple',
-    label: 'Cohere'
+    label: 'Cohere',
   },
   { value: 39, color: 'grey', label: 'Cloudflare' },
   { value: 43, color: 'blue', label: 'DeepSeek' },
   {
     value: 15,
     color: 'blue',
-    label: '百度文心千帆'
+    label: '百度文心千帆',
   },
   {
     value: 46,
     color: 'blue',
-    label: '百度文心千帆V2'
+    label: '百度文心千帆V2',
   },
   {
     value: 17,
     color: 'orange',
-    label: '阿里通义千问'
+    label: '阿里通义千问',
   },
   {
     value: 18,
     color: 'blue',
-    label: '讯飞星火认知'
+    label: '讯飞星火认知',
   },
   {
     value: 16,
     color: 'violet',
-    label: '智谱 ChatGLM'
+    label: '智谱 ChatGLM',
   },
   {
     value: 26,
     color: 'purple',
-    label: '智谱 GLM-4V'
+    label: '智谱 GLM-4V',
   },
   {
     value: 24,
     color: 'orange',
-    label: 'Google Gemini'
+    label: 'Google Gemini',
   },
   {
     value: 11,
     color: 'orange',
-    label: 'Google PaLM2'
+    label: 'Google PaLM2',
   },
   {
     value: 47,
     color: 'blue',
-    label: 'Xinference'
+    label: 'Xinference',
   },
   { value: 25, color: 'green', label: 'Moonshot' },
   { value: 20, color: 'green', label: 'OpenRouter' },
@@ -98,22 +98,22 @@ export const CHANNEL_OPTIONS = [
   {
     value: 22,
     color: 'blue',
-    label: '知识库:FastGPT'
+    label: '知识库:FastGPT',
   },
   {
     value: 21,
     color: 'purple',
-    label: '知识库:AI Proxy'
+    label: '知识库:AI Proxy',
   },
   {
     value: 44,
     color: 'purple',
-    label: '嵌入模型:MokaAI M3E'
+    label: '嵌入模型:MokaAI M3E',
   },
   {
     value: 45,
     color: 'blue',
-    label: '字节火山方舟、豆包、DeepSeek通用'
+    label: '字节火山方舟、豆包、DeepSeek通用',
   },
   {
     value: 48,

+ 18 - 12
web/src/context/Style/index.js

@@ -19,25 +19,25 @@ export const StyleProvider = ({ children }) => {
     if ('type' in action) {
       switch (action.type) {
         case 'TOGGLE_SIDER':
-          setState(prev => ({ ...prev, showSider: !prev.showSider }));
+          setState((prev) => ({ ...prev, showSider: !prev.showSider }));
           break;
         case 'SET_SIDER':
-          setState(prev => ({ ...prev, showSider: action.payload }));
+          setState((prev) => ({ ...prev, showSider: action.payload }));
           break;
         case 'SET_MOBILE':
-          setState(prev => ({ ...prev, isMobile: action.payload }));
+          setState((prev) => ({ ...prev, isMobile: action.payload }));
           break;
         case 'SET_SIDER_COLLAPSED':
-          setState(prev => ({ ...prev, siderCollapsed: action.payload }));
-          break
+          setState((prev) => ({ ...prev, siderCollapsed: action.payload }));
+          break;
         case 'SET_INNER_PADDING':
-          setState(prev => ({ ...prev, shouldInnerPadding: action.payload }));
+          setState((prev) => ({ ...prev, shouldInnerPadding: action.payload }));
           break;
         default:
-          setState(prev => ({ ...prev, ...action }));
+          setState((prev) => ({ ...prev, ...action }));
       }
     } else {
-      setState(prev => ({ ...prev, ...action }));
+      setState((prev) => ({ ...prev, ...action }));
     }
   };
 
@@ -45,7 +45,7 @@ export const StyleProvider = ({ children }) => {
     const updateIsMobile = () => {
       const mobileDetected = isMobile();
       dispatch({ type: 'SET_MOBILE', payload: mobileDetected });
-      
+
       // If on mobile, we might want to auto-hide the sidebar
       if (mobileDetected && state.showSider) {
         dispatch({ type: 'SET_SIDER', payload: false });
@@ -57,7 +57,12 @@ export const StyleProvider = ({ children }) => {
     const updateShowSider = () => {
       // check pathname
       const pathname = window.location.pathname;
-      if (pathname === '' || pathname === '/' || pathname.includes('/home') || pathname.includes('/chat')) {
+      if (
+        pathname === '' ||
+        pathname === '/' ||
+        pathname.includes('/home') ||
+        pathname.includes('/chat')
+      ) {
         dispatch({ type: 'SET_SIDER', payload: false });
         dispatch({ type: 'SET_INNER_PADDING', payload: false });
       } else if (pathname === '/setup') {
@@ -73,7 +78,8 @@ export const StyleProvider = ({ children }) => {
     updateShowSider();
 
     const updateSiderCollapsed = () => {
-      const isCollapsed = localStorage.getItem('default_collapse_sidebar') === 'true';
+      const isCollapsed =
+        localStorage.getItem('default_collapse_sidebar') === 'true';
       dispatch({ type: 'SET_SIDER_COLLAPSED', payload: isCollapsed });
     };
 
@@ -83,7 +89,7 @@ export const StyleProvider = ({ children }) => {
     const handleResize = () => {
       updateIsMobile();
     };
-    
+
     window.addEventListener('resize', handleResize);
 
     // Cleanup event listener on component unmount

+ 4 - 4
web/src/helpers/api.js

@@ -7,8 +7,8 @@ export let API = axios.create({
     : '',
   headers: {
     'New-API-User': getUserIdFromLocalStorage(),
-    'Cache-Control': 'no-store'
-  }
+    'Cache-Control': 'no-store',
+  },
 });
 
 export function updateAPI() {
@@ -18,8 +18,8 @@ export function updateAPI() {
       : '',
     headers: {
       'New-API-User': getUserIdFromLocalStorage(),
-      'Cache-Control': 'no-store'
-    }
+      'Cache-Control': 'no-store',
+    },
   });
 }
 

+ 7 - 7
web/src/helpers/other.js

@@ -1,7 +1,7 @@
-export function getLogOther(otherStr) {
-    if (otherStr === undefined || otherStr === '') {
-        otherStr = '{}'
-    }
-    let other = JSON.parse(otherStr)
-    return other
-}
+export function getLogOther(otherStr) {
+  if (otherStr === undefined || otherStr === '') {
+    otherStr = '{}';
+  }
+  let other = JSON.parse(otherStr);
+  return other;
+}

+ 342 - 225
web/src/helpers/render.js

@@ -44,7 +44,10 @@ export function renderGroup(group) {
             if (await copy(group)) {
               showSuccess(i18next.t('已复制:') + group);
             } else {
-              Modal.error({ title: t('无法复制到剪贴板,请手动复制'), content: group });
+              Modal.error({
+                title: t('无法复制到剪贴板,请手动复制'),
+                content: group,
+              });
             }
           }}
         >
@@ -64,28 +67,37 @@ export function renderRatio(ratio) {
   } else if (ratio > 1) {
     color = 'blue';
   }
-  return <Tag color={color}>{ratio}x {i18next.t('倍率')}</Tag>;
+  return (
+    <Tag color={color}>
+      {ratio}x {i18next.t('倍率')}
+    </Tag>
+  );
 }
 
-const measureTextWidth = (text, style = {
-  fontSize: '14px',
-  fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
-}, containerWidth) => {
+const measureTextWidth = (
+  text,
+  style = {
+    fontSize: '14px',
+    fontFamily:
+      '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
+  },
+  containerWidth,
+) => {
   const span = document.createElement('span');
-  
+
   span.style.visibility = 'hidden';
   span.style.position = 'absolute';
   span.style.whiteSpace = 'nowrap';
   span.style.fontSize = style.fontSize;
   span.style.fontFamily = style.fontFamily;
-  
+
   span.textContent = text;
-  
+
   document.body.appendChild(span);
   const width = span.offsetWidth;
-  
+
   document.body.removeChild(span);
-  
+
   return width;
 };
 
@@ -94,7 +106,7 @@ export function truncateText(text, maxWidth = 200) {
     return text;
   }
   if (!text) return text;
-  
+
   try {
     // Handle percentage-based maxWidth
     let actualMaxWidth = maxWidth;
@@ -103,19 +115,19 @@ export function truncateText(text, maxWidth = 200) {
       // Use window width as fallback container width
       actualMaxWidth = window.innerWidth * percentage;
     }
-    
+
     const width = measureTextWidth(text);
     if (width <= actualMaxWidth) return text;
-    
+
     let left = 0;
     let right = text.length;
     let result = text;
-    
+
     while (left <= right) {
       const mid = Math.floor((left + right) / 2);
       const truncated = text.slice(0, mid) + '...';
       const currentWidth = measureTextWidth(truncated);
-      
+
       if (currentWidth <= actualMaxWidth) {
         result = truncated;
         left = mid + 1;
@@ -123,10 +135,13 @@ export function truncateText(text, maxWidth = 200) {
         right = mid - 1;
       }
     }
-    
+
     return result;
   } catch (error) {
-    console.warn('Text measurement failed, falling back to character count', error);
+    console.warn(
+      'Text measurement failed, falling back to character count',
+      error,
+    );
     if (text.length > 20) {
       return text.slice(0, 17) + '...';
     }
@@ -149,11 +164,11 @@ export const renderGroupOption = (item) => {
     emptyContent,
     ...rest
   } = item;
-  
+
   const baseStyle = {
-    display: 'flex', 
-    justifyContent: 'space-between', 
-    alignItems: 'center', 
+    display: 'flex',
+    justifyContent: 'space-between',
+    alignItems: 'center',
     padding: '8px 16px',
     cursor: disabled ? 'not-allowed' : 'pointer',
     backgroundColor: focused ? 'var(--semi-color-fill-0)' : 'transparent',
@@ -162,8 +177,8 @@ export const renderGroupOption = (item) => {
       backgroundColor: 'var(--semi-color-primary-light-default)',
     }),
     '&:hover': {
-      backgroundColor: !disabled && 'var(--semi-color-fill-1)'
-    }
+      backgroundColor: !disabled && 'var(--semi-color-fill-1)',
+    },
   };
 
   const handleClick = () => {
@@ -177,9 +192,9 @@ export const renderGroupOption = (item) => {
       onMouseEnter(e);
     }
   };
-  
+
   return (
-    <div 
+    <div
       style={baseStyle}
       onClick={handleClick}
       onMouseEnter={handleMouseEnter}
@@ -188,7 +203,7 @@ export const renderGroupOption = (item) => {
         <Typography.Text strong type={disabled ? 'tertiary' : undefined}>
           {value}
         </Typography.Text>
-        <Typography.Text type="secondary" size="small">
+        <Typography.Text type='secondary' size='small'>
           {label}
         </Typography.Text>
       </div>
@@ -222,8 +237,7 @@ export function renderQuotaNumberWithDigit(num, digits = 2) {
 }
 
 export function renderNumberWithPoint(num) {
-  if (num === undefined)
-    return '';
+  if (num === undefined) return '';
   num = num.toFixed(2);
   if (num >= 100000) {
     // Convert number to string to manipulate it
@@ -302,11 +316,14 @@ export function renderModelPrice(
   cacheRatio = 1.0,
 ) {
   if (modelPrice !== -1) {
-    return i18next.t('模型价格:${{price}} * 分组倍率:{{ratio}} = ${{total}}', {
-      price: modelPrice,
-      ratio: groupRatio,
-      total: modelPrice * groupRatio
-    });
+    return i18next.t(
+      '模型价格:${{price}} * 分组倍率:{{ratio}} = ${{total}}',
+      {
+        price: modelPrice,
+        ratio: groupRatio,
+        total: modelPrice * groupRatio,
+      },
+    );
   } else {
     if (completionRatio === undefined) {
       completionRatio = 0;
@@ -314,54 +331,72 @@ export function renderModelPrice(
     let inputRatioPrice = modelRatio * 2.0;
     let completionRatioPrice = modelRatio * 2.0 * completionRatio;
     let cacheRatioPrice = modelRatio * 2.0 * cacheRatio;
-    
+
     // Calculate effective input tokens (non-cached + cached with ratio applied)
-    const effectiveInputTokens = (inputTokens - cacheTokens) + (cacheTokens * cacheRatio);
-    
+    const effectiveInputTokens =
+      inputTokens - cacheTokens + cacheTokens * cacheRatio;
+
     let price =
       (effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio +
       (completionTokens / 1000000) * completionRatioPrice * groupRatio;
-    
+
     return (
       <>
         <article>
-          <p>{i18next.t('提示价格:${{price}} / 1M tokens', {
-            price: inputRatioPrice,
-          })}</p>
-          <p>{i18next.t('补全价格:${{price}} * {{completionRatio}} = ${{total}} / 1M tokens (补全倍率: {{completionRatio}})', {
-            price: inputRatioPrice,
-            total: completionRatioPrice,
-            completionRatio: completionRatio
-          })}</p>
-          {cacheTokens > 0 && (
-            <p>{i18next.t('缓存价格:${{price}} * {{cacheRatio}} = ${{total}} / 1M tokens (缓存倍率: {{cacheRatio}})', {
+          <p>
+            {i18next.t('提示价格:${{price}} / 1M tokens', {
               price: inputRatioPrice,
-              total: inputRatioPrice * cacheRatio,
-              cacheRatio: cacheRatio
-            })}</p>
+            })}
+          </p>
+          <p>
+            {i18next.t(
+              '补全价格:${{price}} * {{completionRatio}} = ${{total}} / 1M tokens (补全倍率: {{completionRatio}})',
+              {
+                price: inputRatioPrice,
+                total: completionRatioPrice,
+                completionRatio: completionRatio,
+              },
+            )}
+          </p>
+          {cacheTokens > 0 && (
+            <p>
+              {i18next.t(
+                '缓存价格:${{price}} * {{cacheRatio}} = ${{total}} / 1M tokens (缓存倍率: {{cacheRatio}})',
+                {
+                  price: inputRatioPrice,
+                  total: inputRatioPrice * cacheRatio,
+                  cacheRatio: cacheRatio,
+                },
+              )}
+            </p>
           )}
           <p></p>
           <p>
-            {cacheTokens > 0 ? 
-              i18next.t('提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}', {
-                nonCacheInput: inputTokens - cacheTokens,
-                cacheInput: cacheTokens,
-                cachePrice: inputRatioPrice * cacheRatio,
-                price: inputRatioPrice,
-                completion: completionTokens,
-                compPrice: completionRatioPrice,
-                ratio: groupRatio,
-                total: price.toFixed(6)
-              }) :
-              i18next.t('提示 {{input}} tokens / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}', {
-                input: inputTokens,
-                price: inputRatioPrice,
-                completion: completionTokens,
-                compPrice: completionRatioPrice,
-                ratio: groupRatio,
-                total: price.toFixed(6)
-              })
-            }
+            {cacheTokens > 0
+              ? i18next.t(
+                  '提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
+                  {
+                    nonCacheInput: inputTokens - cacheTokens,
+                    cacheInput: cacheTokens,
+                    cachePrice: inputRatioPrice * cacheRatio,
+                    price: inputRatioPrice,
+                    completion: completionTokens,
+                    compPrice: completionRatioPrice,
+                    ratio: groupRatio,
+                    total: price.toFixed(6),
+                  },
+                )
+              : i18next.t(
+                  '提示 {{input}} tokens / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
+                  {
+                    input: inputTokens,
+                    price: inputRatioPrice,
+                    completion: completionTokens,
+                    compPrice: completionRatioPrice,
+                    ratio: groupRatio,
+                    total: price.toFixed(6),
+                  },
+                )}
           </p>
           <p>{i18next.t('仅供参考,以实际扣费为准')}</p>
         </article>
@@ -380,19 +415,22 @@ export function renderModelPriceSimple(
   if (modelPrice !== -1) {
     return i18next.t('价格:${{price}} * 分组:{{ratio}}', {
       price: modelPrice,
-      ratio: groupRatio
+      ratio: groupRatio,
     });
   } else {
     if (cacheTokens !== 0) {
-      return i18next.t('模型: {{ratio}} * 分组: {{groupRatio}} * 缓存: {{cacheRatio}}', {
-        ratio: modelRatio,
-        groupRatio: groupRatio,
-        cacheRatio: cacheRatio
-      });
+      return i18next.t(
+        '模型: {{ratio}} * 分组: {{groupRatio}} * 缓存: {{cacheRatio}}',
+        {
+          ratio: modelRatio,
+          groupRatio: groupRatio,
+          cacheRatio: cacheRatio,
+        },
+      );
     } else {
       return i18next.t('模型: {{ratio}} * 分组: {{groupRatio}}', {
         ratio: modelRatio,
-        groupRatio: groupRatio
+        groupRatio: groupRatio,
       });
     }
   }
@@ -414,11 +452,14 @@ export function renderAudioModelPrice(
 ) {
   // 1 ratio = $0.002 / 1K tokens
   if (modelPrice !== -1) {
-    return i18next.t('模型价格:${{price}} * 分组倍率:{{ratio}} = ${{total}}', {
-      price: modelPrice,
-      ratio: groupRatio,
-      total: modelPrice * groupRatio
-    });
+    return i18next.t(
+      '模型价格:${{price}} * 分组倍率:{{ratio}} = ${{total}}',
+      {
+        price: modelPrice,
+        ratio: groupRatio,
+        total: modelPrice * groupRatio,
+      },
+    );
   } else {
     if (completionRatio === undefined) {
       completionRatio = 0;
@@ -430,81 +471,120 @@ export function renderAudioModelPrice(
     let inputRatioPrice = modelRatio * 2.0;
     let completionRatioPrice = modelRatio * 2.0 * completionRatio;
     let cacheRatioPrice = modelRatio * 2.0 * cacheRatio;
-    
+
     // Calculate effective input tokens (non-cached + cached with ratio applied)
-    const effectiveInputTokens = (inputTokens - cacheTokens) + (cacheTokens * cacheRatio);
-    
+    const effectiveInputTokens =
+      inputTokens - cacheTokens + cacheTokens * cacheRatio;
+
     let textPrice =
       (effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio +
-      (completionTokens / 1000000) * completionRatioPrice * groupRatio
+      (completionTokens / 1000000) * completionRatioPrice * groupRatio;
     let audioPrice =
       (audioInputTokens / 1000000) * inputRatioPrice * audioRatio * groupRatio +
-      (audioCompletionTokens / 1000000) * inputRatioPrice * audioRatio * audioCompletionRatio * groupRatio;
+      (audioCompletionTokens / 1000000) *
+        inputRatioPrice *
+        audioRatio *
+        audioCompletionRatio *
+        groupRatio;
     let price = textPrice + audioPrice;
     return (
       <>
         <article>
-          <p>{i18next.t('提示价格:${{price}} / 1M tokens', {
-            price: inputRatioPrice,
-          })}</p>
-          <p>{i18next.t('补全价格:${{price}} * {{completionRatio}} = ${{total}} / 1M tokens (补全倍率: {{completionRatio}})', {
-            price: inputRatioPrice,
-            total: completionRatioPrice,
-            completionRatio: completionRatio
-          })}</p>
-          {cacheTokens > 0 && (
-            <p>{i18next.t('缓存价格:${{price}} * {{cacheRatio}} = ${{total}} / 1M tokens (缓存倍率: {{cacheRatio}})', {
+          <p>
+            {i18next.t('提示价格:${{price}} / 1M tokens', {
               price: inputRatioPrice,
-              total: inputRatioPrice * cacheRatio,
-              cacheRatio: cacheRatio
-            })}</p>
+            })}
+          </p>
+          <p>
+            {i18next.t(
+              '补全价格:${{price}} * {{completionRatio}} = ${{total}} / 1M tokens (补全倍率: {{completionRatio}})',
+              {
+                price: inputRatioPrice,
+                total: completionRatioPrice,
+                completionRatio: completionRatio,
+              },
+            )}
+          </p>
+          {cacheTokens > 0 && (
+            <p>
+              {i18next.t(
+                '缓存价格:${{price}} * {{cacheRatio}} = ${{total}} / 1M tokens (缓存倍率: {{cacheRatio}})',
+                {
+                  price: inputRatioPrice,
+                  total: inputRatioPrice * cacheRatio,
+                  cacheRatio: cacheRatio,
+                },
+              )}
+            </p>
           )}
-          <p>{i18next.t('音频提示价格:${{price}} * {{audioRatio}} = ${{total}} / 1M tokens (音频倍率: {{audioRatio}})', {
-            price: inputRatioPrice,
-            total: inputRatioPrice * audioRatio,
-            audioRatio: audioRatio
-          })}</p>
-          <p>{i18next.t('音频补全价格:${{price}} * {{audioRatio}} * {{audioCompRatio}} = ${{total}} / 1M tokens (音频补全倍率: {{audioCompRatio}})', {
-            price: inputRatioPrice,
-            total: inputRatioPrice * audioRatio * audioCompletionRatio,
-            audioRatio: audioRatio,
-            audioCompRatio: audioCompletionRatio
-          })}</p>
           <p>
-            {cacheTokens > 0 ? 
-              i18next.t('文字提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}', {
-                nonCacheInput: inputTokens - cacheTokens,
-                cacheInput: cacheTokens,
-                cachePrice: inputRatioPrice * cacheRatio,
+            {i18next.t(
+              '音频提示价格:${{price}} * {{audioRatio}} = ${{total}} / 1M tokens (音频倍率: {{audioRatio}})',
+              {
                 price: inputRatioPrice,
-                completion: completionTokens,
-                compPrice: completionRatioPrice,
-                total: textPrice.toFixed(6)
-              }) :
-              i18next.t('文字提示 {{input}} tokens / 1M tokens * ${{price}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}', {
-                input: inputTokens,
+                total: inputRatioPrice * audioRatio,
+                audioRatio: audioRatio,
+              },
+            )}
+          </p>
+          <p>
+            {i18next.t(
+              '音频补全价格:${{price}} * {{audioRatio}} * {{audioCompRatio}} = ${{total}} / 1M tokens (音频补全倍率: {{audioCompRatio}})',
+              {
                 price: inputRatioPrice,
-                completion: completionTokens,
-                compPrice: completionRatioPrice,
-                total: textPrice.toFixed(6)
-              })
-            }
+                total: inputRatioPrice * audioRatio * audioCompletionRatio,
+                audioRatio: audioRatio,
+                audioCompRatio: audioCompletionRatio,
+              },
+            )}
           </p>
           <p>
-            {i18next.t('音频提示 {{input}} tokens / 1M tokens * ${{audioInputPrice}} + 音频补全 {{completion}} tokens / 1M tokens * ${{audioCompPrice}} = ${{total}}', {
-              input: audioInputTokens,
-              completion: audioCompletionTokens,
-              audioInputPrice: audioRatio * inputRatioPrice,
-              audioCompPrice: audioRatio * audioCompletionRatio * inputRatioPrice,
-              total: audioPrice.toFixed(6)
-            })}
+            {cacheTokens > 0
+              ? i18next.t(
+                  '文字提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}',
+                  {
+                    nonCacheInput: inputTokens - cacheTokens,
+                    cacheInput: cacheTokens,
+                    cachePrice: inputRatioPrice * cacheRatio,
+                    price: inputRatioPrice,
+                    completion: completionTokens,
+                    compPrice: completionRatioPrice,
+                    total: textPrice.toFixed(6),
+                  },
+                )
+              : i18next.t(
+                  '文字提示 {{input}} tokens / 1M tokens * ${{price}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}',
+                  {
+                    input: inputTokens,
+                    price: inputRatioPrice,
+                    completion: completionTokens,
+                    compPrice: completionRatioPrice,
+                    total: textPrice.toFixed(6),
+                  },
+                )}
           </p>
           <p>
-            {i18next.t('总价:文字价格 {{textPrice}} + 音频价格 {{audioPrice}} = ${{total}}', {
-              total: price.toFixed(6),
-              textPrice: textPrice.toFixed(6),
-              audioPrice: audioPrice.toFixed(6)
-            })}
+            {i18next.t(
+              '音频提示 {{input}} tokens / 1M tokens * ${{audioInputPrice}} + 音频补全 {{completion}} tokens / 1M tokens * ${{audioCompPrice}} = ${{total}}',
+              {
+                input: audioInputTokens,
+                completion: audioCompletionTokens,
+                audioInputPrice: audioRatio * inputRatioPrice,
+                audioCompPrice:
+                  audioRatio * audioCompletionRatio * inputRatioPrice,
+                total: audioPrice.toFixed(6),
+              },
+            )}
+          </p>
+          <p>
+            {i18next.t(
+              '总价:文字价格 {{textPrice}} + 音频价格 {{audioPrice}} = ${{total}}',
+              {
+                total: price.toFixed(6),
+                textPrice: textPrice.toFixed(6),
+                audioPrice: audioPrice.toFixed(6),
+              },
+            )}
           </p>
           <p>{i18next.t('仅供参考,以实际扣费为准')}</p>
         </article>
@@ -517,7 +597,9 @@ export function renderQuotaWithPrompt(quota, digits) {
   let displayInCurrency = localStorage.getItem('display_in_currency');
   displayInCurrency = displayInCurrency === 'true';
   if (displayInCurrency) {
-    return ' | ' + i18next.t('等价金额') + ': ' + renderQuota(quota, digits) + '';
+    return (
+      ' | ' + i18next.t('等价金额') + ': ' + renderQuota(quota, digits) + ''
+    );
   }
   return '';
 }
@@ -537,7 +619,7 @@ const colors = [
   'red',
   'teal',
   'violet',
-  'yellow'
+  'yellow',
 ];
 
 // 基础10色色板 (N ≤ 10)
@@ -551,7 +633,7 @@ const baseColors = [
   '#304D77',
   '#B48DEB',
   '#009488',
-  '#FF7DDA'
+  '#FF7DDA',
 ];
 
 // 扩展20色色板 (10 < N ≤ 20)
@@ -575,7 +657,7 @@ const extendedColors = [
   '#009488',
   '#59BAA8',
   '#FF7DDA',
-  '#FFCFEE'
+  '#FFCFEE',
 ];
 
 export const modelColorMap = {
@@ -631,14 +713,14 @@ export function modelToColor(modelName) {
   // 2. 生成一个稳定的数字作为索引
   let hash = 0;
   for (let i = 0; i < modelName.length; i++) {
-    hash = ((hash << 5) - hash) + modelName.charCodeAt(i);
+    hash = (hash << 5) - hash + modelName.charCodeAt(i);
     hash = hash & hash; // Convert to 32-bit integer
   }
   hash = Math.abs(hash);
 
   // 3. 根据模型名称长度选择不同的色板
   const colorPalette = modelName.length > 10 ? extendedColors : baseColors;
-  
+
   // 4. 使用hash值选择颜色
   const index = hash % colorPalette.length;
   return colorPalette[index];
@@ -668,12 +750,15 @@ export function renderClaudeModelPrice(
   const ratioLabel = false ? i18next.t('专属倍率') : i18next.t('分组倍率');
 
   if (modelPrice !== -1) {
-    return i18next.t('模型价格:${{price}} * {{ratioType}}:{{ratio}} = ${{total}}', {
-      price: modelPrice,
-      ratioType: ratioLabel,
-      ratio: groupRatio,
-      total: modelPrice * groupRatio
-    });
+    return i18next.t(
+      '模型价格:${{price}} * {{ratioType}}:{{ratio}} = ${{total}}',
+      {
+        price: modelPrice,
+        ratioType: ratioLabel,
+        ratio: groupRatio,
+        total: modelPrice * groupRatio,
+      },
+    );
   } else {
     if (completionRatio === undefined) {
       completionRatio = 0;
@@ -687,9 +772,10 @@ export function renderClaudeModelPrice(
 
     // Calculate effective input tokens (non-cached + cached with ratio applied + cache creation with ratio applied)
     const nonCachedTokens = inputTokens;
-    const effectiveInputTokens = nonCachedTokens +
-      (cacheTokens * cacheRatio) +
-      (cacheCreationTokens * cacheCreationRatio);
+    const effectiveInputTokens =
+      nonCachedTokens +
+      cacheTokens * cacheRatio +
+      cacheCreationTokens * cacheCreationRatio;
 
     let price =
       (effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio +
@@ -698,56 +784,78 @@ export function renderClaudeModelPrice(
     return (
       <>
         <article>
-          <p>{i18next.t('提示价格:${{price}} / 1M tokens', {
-            price: inputRatioPrice,
-          })}</p>
-          <p>{i18next.t('补全价格:${{price}} * {{ratio}} = ${{total}} / 1M tokens', {
-            price: inputRatioPrice,
-            ratio: completionRatio,
-            total: completionRatioPrice
-          })}</p>
-          {cacheTokens > 0 && (
-            <p>{i18next.t('缓存价格:${{price}} * {{ratio}} = ${{total}} / 1M tokens (缓存倍率: {{cacheRatio}})', {
+          <p>
+            {i18next.t('提示价格:${{price}} / 1M tokens', {
               price: inputRatioPrice,
-              ratio: cacheRatio,
-              total: cacheRatioPrice,
-              cacheRatio: cacheRatio
-            })}</p>
+            })}
+          </p>
+          <p>
+            {i18next.t(
+              '补全价格:${{price}} * {{ratio}} = ${{total}} / 1M tokens',
+              {
+                price: inputRatioPrice,
+                ratio: completionRatio,
+                total: completionRatioPrice,
+              },
+            )}
+          </p>
+          {cacheTokens > 0 && (
+            <p>
+              {i18next.t(
+                '缓存价格:${{price}} * {{ratio}} = ${{total}} / 1M tokens (缓存倍率: {{cacheRatio}})',
+                {
+                  price: inputRatioPrice,
+                  ratio: cacheRatio,
+                  total: cacheRatioPrice,
+                  cacheRatio: cacheRatio,
+                },
+              )}
+            </p>
           )}
           {cacheCreationTokens > 0 && (
-            <p>{i18next.t('缓存创建价格:${{price}} * {{ratio}} = ${{total}} / 1M tokens (缓存创建倍率: {{cacheCreationRatio}})', {
-              price: inputRatioPrice,
-              ratio: cacheCreationRatio,
-              total: cacheCreationRatioPrice,
-              cacheCreationRatio: cacheCreationRatio
-            })}</p>
+            <p>
+              {i18next.t(
+                '缓存创建价格:${{price}} * {{ratio}} = ${{total}} / 1M tokens (缓存创建倍率: {{cacheCreationRatio}})',
+                {
+                  price: inputRatioPrice,
+                  ratio: cacheCreationRatio,
+                  total: cacheCreationRatioPrice,
+                  cacheCreationRatio: cacheCreationRatio,
+                },
+              )}
+            </p>
           )}
           <p></p>
           <p>
-            {(cacheTokens > 0 || cacheCreationTokens > 0) ?
-              i18next.t('提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 缓存创建 {{cacheCreationInput}} tokens / 1M tokens * ${{cacheCreationPrice}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}', {
-                nonCacheInput: nonCachedTokens,
-                cacheInput: cacheTokens,
-                cacheRatio: cacheRatio,
-                cacheCreationInput: cacheCreationTokens,
-                cacheCreationRatio: cacheCreationRatio,
-                cachePrice: cacheRatioPrice,
-                cacheCreationPrice: cacheCreationRatioPrice,
-                price: inputRatioPrice,
-                completion: completionTokens,
-                compPrice: completionRatioPrice,
-                ratio: groupRatio,
-                total: price.toFixed(6)
-              }) :
-              i18next.t('提示 {{input}} tokens / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}', {
-                input: inputTokens,
-                price: inputRatioPrice,
-                completion: completionTokens,
-                compPrice: completionRatioPrice,
-                ratio: groupRatio,
-                total: price.toFixed(6)
-              })
-            }
+            {cacheTokens > 0 || cacheCreationTokens > 0
+              ? i18next.t(
+                  '提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 缓存创建 {{cacheCreationInput}} tokens / 1M tokens * ${{cacheCreationPrice}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
+                  {
+                    nonCacheInput: nonCachedTokens,
+                    cacheInput: cacheTokens,
+                    cacheRatio: cacheRatio,
+                    cacheCreationInput: cacheCreationTokens,
+                    cacheCreationRatio: cacheCreationRatio,
+                    cachePrice: cacheRatioPrice,
+                    cacheCreationPrice: cacheCreationRatioPrice,
+                    price: inputRatioPrice,
+                    completion: completionTokens,
+                    compPrice: completionRatioPrice,
+                    ratio: groupRatio,
+                    total: price.toFixed(6),
+                  },
+                )
+              : i18next.t(
+                  '提示 {{input}} tokens / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
+                  {
+                    input: inputTokens,
+                    price: inputRatioPrice,
+                    completion: completionTokens,
+                    compPrice: completionRatioPrice,
+                    ratio: groupRatio,
+                    total: price.toFixed(6),
+                  },
+                )}
           </p>
           <p>{i18next.t('仅供参考,以实际扣费为准')}</p>
         </article>
@@ -770,17 +878,20 @@ export function renderClaudeLogContent(
     return i18next.t('模型价格 ${{price}},{{ratioType}} {{ratio}}', {
       price: modelPrice,
       ratioType: ratioLabel,
-      ratio: groupRatio
+      ratio: groupRatio,
     });
   } else {
-    return i18next.t('模型倍率 {{modelRatio}},补全倍率 {{completionRatio}},缓存倍率 {{cacheRatio}},缓存创建倍率 {{cacheCreationRatio}},{{ratioType}} {{ratio}}', {
-      modelRatio: modelRatio,
-      completionRatio: completionRatio,
-      cacheRatio: cacheRatio,
-      cacheCreationRatio: cacheCreationRatio,
-      ratioType: ratioLabel,
-      ratio: groupRatio
-    });
+    return i18next.t(
+      '模型倍率 {{modelRatio}},补全倍率 {{completionRatio}},缓存倍率 {{cacheRatio}},缓存创建倍率 {{cacheCreationRatio}},{{ratioType}} {{ratio}}',
+      {
+        modelRatio: modelRatio,
+        completionRatio: completionRatio,
+        cacheRatio: cacheRatio,
+        cacheCreationRatio: cacheCreationRatio,
+        ratioType: ratioLabel,
+        ratio: groupRatio,
+      },
+    );
   }
 }
 
@@ -799,22 +910,25 @@ export function renderClaudeModelPriceSimple(
     return i18next.t('价格:${{price}} * {{ratioType}}:{{ratio}}', {
       price: modelPrice,
       ratioType: ratioLabel,
-      ratio: groupRatio
+      ratio: groupRatio,
     });
   } else {
     if (cacheTokens !== 0 || cacheCreationTokens !== 0) {
-      return i18next.t('模型: {{ratio}} * {{ratioType}}: {{groupRatio}} * 缓存: {{cacheRatio}}', {
-        ratio: modelRatio,
-        ratioType: ratioLabel,
-        groupRatio: groupRatio,
-        cacheRatio: cacheRatio,
-        cacheCreationRatio: cacheCreationRatio
-      });
+      return i18next.t(
+        '模型: {{ratio}} * {{ratioType}}: {{groupRatio}} * 缓存: {{cacheRatio}}',
+        {
+          ratio: modelRatio,
+          ratioType: ratioLabel,
+          groupRatio: groupRatio,
+          cacheRatio: cacheRatio,
+          cacheCreationRatio: cacheCreationRatio,
+        },
+      );
     } else {
       return i18next.t('模型: {{ratio}} * {{ratioType}}: {{groupRatio}}', {
         ratio: modelRatio,
         ratioType: ratioLabel,
-        groupRatio: groupRatio
+        groupRatio: groupRatio,
       });
     }
   }
@@ -824,7 +938,7 @@ export function renderLogContent(
   modelRatio,
   completionRatio,
   modelPrice = -1,
-  groupRatio
+  groupRatio,
 ) {
   const ratioLabel = false ? i18next.t('专属倍率') : i18next.t('分组倍率');
 
@@ -832,14 +946,17 @@ export function renderLogContent(
     return i18next.t('模型价格 ${{price}},{{ratioType}} {{ratio}}', {
       price: modelPrice,
       ratioType: ratioLabel,
-      ratio: groupRatio
+      ratio: groupRatio,
     });
   } else {
-    return i18next.t('模型倍率 {{modelRatio}},补全倍率 {{completionRatio}},{{ratioType}} {{ratio}}', {
-      modelRatio: modelRatio,
-      completionRatio: completionRatio,
-      ratioType: ratioLabel,
-      ratio: groupRatio
-    });
+    return i18next.t(
+      '模型倍率 {{modelRatio}},补全倍率 {{completionRatio}},{{ratioType}} {{ratio}}',
+      {
+        modelRatio: modelRatio,
+        completionRatio: completionRatio,
+        ratioType: ratioLabel,
+        ratio: groupRatio,
+      },
+    );
   }
 }

+ 3 - 4
web/src/helpers/utils.js

@@ -51,11 +51,11 @@ export async function copy(text) {
   } catch (e) {
     try {
       // 构建input 执行 复制命令
-      var _input = window.document.createElement("input");
+      var _input = window.document.createElement('input');
       _input.value = text;
       window.document.body.appendChild(_input);
       _input.select();
-      window.document.execCommand("Copy");
+      window.document.execCommand('Copy');
       window.document.body.removeChild(_input);
     } catch (e) {
       okay = false;
@@ -191,7 +191,7 @@ export function timestamp2string1(timestamp, dataExportDefaultTime = 'hour') {
   let day = date.getDate().toString();
   let hour = date.getHours().toString();
   if (day === '24') {
-    console.log("timestamp", timestamp);
+    console.log('timestamp', timestamp);
   }
   if (month.length === 1) {
     month = '0' + month;
@@ -247,7 +247,6 @@ export function verifyJSONPromise(value) {
   }
 }
 
-
 export function shouldShowPrompt(id) {
   let prompt = localStorage.getItem(`prompt-${id}`);
   return !prompt;

+ 6 - 6
web/src/i18n/i18n.js

@@ -11,16 +11,16 @@ i18n
   .init({
     resources: {
       en: {
-        translation: enTranslation
+        translation: enTranslation,
       },
       zh: {
-        translation: zhTranslation
-      }
+        translation: zhTranslation,
+      },
     },
     fallbackLng: 'zh',
     interpolation: {
-      escapeValue: false
-    }
+      escapeValue: false,
+    },
   });
 
-export default i18n; 
+export default i18n;

+ 1 - 1
web/src/i18n/locales/en.json

@@ -1065,7 +1065,7 @@
   "价格:${{price}} * 分组:{{ratio}}": "Price: ${{price}} * Group: {{ratio}}",
   "模型: {{ratio}} * 分组: {{groupRatio}}": "Model: {{ratio}} * Group: {{groupRatio}}",
   "统计额度": "Statistical quota",
-  "统计Tokens": "Statistical Tokens", 
+  "统计Tokens": "Statistical Tokens",
   "统计次数": "Statistical count",
   "平均RPM": "Average RPM",
   "平均TPM": "Average TPM",

+ 1 - 1
web/src/i18n/locales/zh.json

@@ -10,4 +10,4 @@
   "展开侧边栏": "展开侧边栏",
   "关闭侧边栏": "关闭侧边栏",
   "注销成功!": "注销成功!"
-} 
+}

+ 57 - 12
web/src/index.css

@@ -1,8 +1,8 @@
 body {
   margin: 0;
   padding-top: 0;
-  font-family: Lato, 'Helvetica Neue', Arial, Helvetica, 'Microsoft YaHei',
-    sans-serif;
+  font-family:
+    Lato, 'Helvetica Neue', Arial, Helvetica, 'Microsoft YaHei', sans-serif;
   -webkit-font-smoothing: antialiased;
   -moz-osx-font-smoothing: grayscale;
   scrollbar-width: none;
@@ -18,7 +18,20 @@ body {
   overflow: hidden;
 }
 
-#root > section > header > section > div > div > div > div.semi-navigation-header-list-outer > div.semi-navigation-list-wrapper > ul > div > a > li > span{
+#root
+  > section
+  > header
+  > section
+  > div
+  > div
+  > div
+  > div.semi-navigation-header-list-outer
+  > div.semi-navigation-list-wrapper
+  > ul
+  > div
+  > a
+  > li
+  > span {
   font-weight: 600 !important;
 }
 
@@ -33,24 +46,56 @@ body {
   .topnav {
     padding: 0 8px;
   }
-  
+
   .topnav .semi-navigation-item {
     margin: 0 1px;
     padding: 0 4px;
   }
-  
+
   .topnav .semi-navigation-list-wrapper {
     max-width: calc(55vw - 20px);
     overflow-x: auto;
     scrollbar-width: none;
   }
-  #root > section > header > section > div > div > div > div.semi-navigation-footer > div > a > li {
+  #root
+    > section
+    > header
+    > section
+    > div
+    > div
+    > div
+    > div.semi-navigation-footer
+    > div
+    > a
+    > li {
     padding: 0 0;
   }
-  #root > section > header > section > div > div > div > div.semi-navigation-header-list-outer > div.semi-navigation-list-wrapper > ul > div > a > li {
+  #root
+    > section
+    > header
+    > section
+    > div
+    > div
+    > div
+    > div.semi-navigation-header-list-outer
+    > div.semi-navigation-list-wrapper
+    > ul
+    > div
+    > a
+    > li {
     padding: 0 5px;
   }
-  #root > section > header > section > div > div > div > div.semi-navigation-footer > div:nth-child(1) > a > li {
+  #root
+    > section
+    > header
+    > section
+    > div
+    > div
+    > div
+    > div.semi-navigation-footer
+    > div:nth-child(1)
+    > a
+    > li {
     padding: 0 5px;
   }
   .semi-navigation-footer {
@@ -96,13 +141,13 @@ body {
     position: static !important;
     height: 100% !important;
   }
-  
+
   /* 确保内容区域在移动端可以正常滚动 */
   #root {
     overflow: visible !important;
     height: 100% !important;
   }
-  
+
   /* 隐藏在移动设备上 */
   .hide-on-mobile {
     display: none !important;
@@ -147,8 +192,8 @@ body::-webkit-scrollbar {
 }
 
 code {
-  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
-    monospace;
+  font-family:
+    source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
 }
 
 .semi-navigation-item {

+ 1 - 1
web/src/index.js

@@ -28,7 +28,7 @@ root.render(
         <BrowserRouter>
           <ThemeProvider>
             <StyleProvider>
-              <PageLayout/>
+              <PageLayout />
             </StyleProvider>
           </ThemeProvider>
         </BrowserRouter>

+ 181 - 145
web/src/pages/Channel/EditChannel.js

@@ -6,8 +6,9 @@ import {
   isMobile,
   showError,
   showInfo,
-  showSuccess, showWarning,
-  verifyJSON
+  showSuccess,
+  showWarning,
+  verifyJSON,
 } from '../../helpers';
 import { CHANNEL_OPTIONS } from '../../constants';
 import Title from '@douyinfe/semi-ui/lib/es/typography/title';
@@ -22,21 +23,22 @@ import {
   Select,
   TextArea,
   Checkbox,
-  Banner, Modal
+  Banner,
+  Modal,
 } from '@douyinfe/semi-ui';
 import { getChannelModels, loadChannelModels } from '../../components/utils.js';
 
 const MODEL_MAPPING_EXAMPLE = {
-  'gpt-3.5-turbo': 'gpt-3.5-turbo-0125'
+  'gpt-3.5-turbo': 'gpt-3.5-turbo-0125',
 };
 
 const STATUS_CODE_MAPPING_EXAMPLE = {
-  400: '500'
+  400: '500',
 };
 
 const REGION_EXAMPLE = {
-  'default': 'us-central1',
-  'claude-3-5-sonnet-20240620': 'europe-west1'
+  default: 'us-central1',
+  'claude-3-5-sonnet-20240620': 'europe-west1',
 };
 
 function type2secretPrompt(type) {
@@ -82,7 +84,7 @@ const EditChannel = (props) => {
     groups: ['default'],
     priority: 0,
     weight: 0,
-    tag: ''
+    tag: '',
   };
   const [batch, setBatch] = useState(false);
   const [autoBan, setAutoBan] = useState(true);
@@ -98,12 +100,13 @@ const EditChannel = (props) => {
     if (name === 'base_url' && value.endsWith('/v1')) {
       Modal.confirm({
         title: '警告',
-        content: '不需要在末尾加/v1,New API会自动处理,添加后可能导致请求失败,是否继续?',
+        content:
+          '不需要在末尾加/v1,New API会自动处理,添加后可能导致请求失败,是否继续?',
         onOk: () => {
           setInputs((inputs) => ({ ...inputs, [name]: value }));
-        }
-      })
-      return
+        },
+      });
+      return;
     }
     setInputs((inputs) => ({ ...inputs, [name]: value }));
     if (name === 'type') {
@@ -117,7 +120,7 @@ const EditChannel = (props) => {
             'mj_blend',
             'mj_upscale',
             'mj_describe',
-            'mj_uploads'
+            'mj_uploads',
           ];
           break;
         case 5:
@@ -137,14 +140,11 @@ const EditChannel = (props) => {
             'mj_high_variation',
             'mj_low_variation',
             'mj_pan',
-            'mj_uploads'
+            'mj_uploads',
           ];
           break;
         case 36:
-          localModels = [
-            'suno_music',
-            'suno_lyrics'
-          ];
+          localModels = ['suno_music', 'suno_lyrics'];
           break;
         default:
           localModels = getChannelModels(value);
@@ -180,7 +180,7 @@ const EditChannel = (props) => {
         data.model_mapping = JSON.stringify(
           JSON.parse(data.model_mapping),
           null,
-          2
+          2,
         );
       }
       setInputs(data);
@@ -197,7 +197,6 @@ const EditChannel = (props) => {
     setLoading(false);
   };
 
-
   const fetchUpstreamModelList = async (name) => {
     // if (inputs['type'] !== 1) {
     //   showError(t('仅支持 OpenAI 接口格式'));
@@ -225,9 +224,9 @@ const EditChannel = (props) => {
           const res = await API.post('/api/channel/fetch_models', {
             base_url: inputs['base_url'],
             type: inputs['type'],
-            key: inputs['key']
+            key: inputs['key'],
           });
-          
+
           if (res.data && res.data.success) {
             models.push(...res.data.data);
           } else {
@@ -254,7 +253,7 @@ const EditChannel = (props) => {
       let res = await API.get(`/api/channel/models`);
       let localModelOptions = res.data.data.map((model) => ({
         label: model.id,
-        value: model.id
+        value: model.id,
       }));
       setOriginModelOptions(localModelOptions);
       setFullModels(res.data.data.map((model) => model.id));
@@ -263,7 +262,7 @@ const EditChannel = (props) => {
           .filter((model) => {
             return model.id.startsWith('gpt-') || model.id.startsWith('text-');
           })
-          .map((model) => model.id)
+          .map((model) => model.id),
       );
     } catch (error) {
       showError(error.message);
@@ -279,8 +278,8 @@ const EditChannel = (props) => {
       setGroupOptions(
         res.data.data.map((group) => ({
           label: group,
-          value: group
-        }))
+          value: group,
+        })),
       );
     } catch (error) {
       showError(error.message);
@@ -293,7 +292,7 @@ const EditChannel = (props) => {
       if (!localModelOptions.find((option) => option.label === model)) {
         localModelOptions.push({
           label: model,
-          value: model
+          value: model,
         });
       }
     });
@@ -304,7 +303,7 @@ const EditChannel = (props) => {
     fetchModels().then();
     fetchGroups().then();
     if (isEdit) {
-      loadChannel().then(() => {});
+      loadChannel().then(() => { });
     } else {
       setInputs(originInputs);
       let localModels = getChannelModels(inputs.type);
@@ -330,7 +329,7 @@ const EditChannel = (props) => {
     if (localInputs.base_url && localInputs.base_url.endsWith('/')) {
       localInputs.base_url = localInputs.base_url.slice(
         0,
-        localInputs.base_url.length - 1
+        localInputs.base_url.length - 1,
       );
     }
     if (localInputs.type === 18 && localInputs.other === '') {
@@ -348,7 +347,7 @@ const EditChannel = (props) => {
     if (isEdit) {
       res = await API.put(`/api/channel/`, {
         ...localInputs,
-        id: parseInt(channelId)
+        id: parseInt(channelId),
       });
     } else {
       res = await API.post(`/api/channel/`, localInputs);
@@ -382,7 +381,7 @@ const EditChannel = (props) => {
         localModelOptions.push({
           key: model,
           text: model,
-          value: model
+          value: model,
         });
       } else if (model) {
         showError(t('某些模型已存在!'));
@@ -397,14 +396,15 @@ const EditChannel = (props) => {
     handleInputChange('models', localModels);
   };
 
-
   return (
     <>
       <SideSheet
         maskClosable={false}
         placement={isEdit ? 'right' : 'left'}
         title={
-          <Title level={3}>{isEdit ? t('更新渠道信息') : t('创建新的渠道')}</Title>
+          <Title level={3}>
+            {isEdit ? t('更新渠道信息') : t('创建新的渠道')}
+          </Title>
         }
         headerStyle={{ borderBottom: '1px solid var(--semi-color-border)' }}
         bodyStyle={{ borderBottom: '1px solid var(--semi-color-border)' }}
@@ -412,11 +412,11 @@ const EditChannel = (props) => {
         footer={
           <div style={{ display: 'flex', justifyContent: 'flex-end' }}>
             <Space>
-              <Button theme="solid" size={'large'} onClick={submit}>
+              <Button theme='solid' size={'large'} onClick={submit}>
                 {t('提交')}
               </Button>
               <Button
-                theme="solid"
+                theme='solid'
                 size={'large'}
                 type={'tertiary'}
                 onClick={handleCancel}
@@ -432,11 +432,10 @@ const EditChannel = (props) => {
       >
         <Spin spinning={loading}>
           <div style={{ marginTop: 10 }}>
-            
             <Typography.Text strong>{t('类型')}:</Typography.Text>
           </div>
           <Select
-            name="type"
+            name='type'
             required
             optionList={CHANNEL_OPTIONS}
             value={inputs.type}
@@ -449,17 +448,17 @@ const EditChannel = (props) => {
           {inputs.type === 40 && (
             <div style={{ marginTop: 10 }}>
               <Banner
-                type="info" 
+                type='info'
                 description={
                   <div>
-                    <Typography.Text strong>
-                      {t('邀请链接')}: 
-                    </Typography.Text>
-                    <Typography.Text 
+                    <Typography.Text strong>{t('邀请链接')}:</Typography.Text>
+                    <Typography.Text
                       link
-                      underline 
-                      style={{marginLeft: 8}}
-                      onClick={() => window.open('https://cloud.siliconflow.cn/i/hij0YNTZ')}
+                      underline
+                      style={{ marginLeft: 8 }}
+                      onClick={() =>
+                        window.open('https://cloud.siliconflow.cn/i/hij0YNTZ')
+                      }
                     >
                       https://cloud.siliconflow.cn/i/hij0YNTZ
                     </Typography.Text>
@@ -482,27 +481,29 @@ const EditChannel = (props) => {
                 </Typography.Text>
               </div>
               <Input
-                label="AZURE_OPENAI_ENDPOINT"
-                name="azure_base_url"
-                placeholder={t('请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com')}
+                label='AZURE_OPENAI_ENDPOINT'
+                name='azure_base_url'
+                placeholder={t(
+                  '请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com',
+                )}
                 onChange={(value) => {
                   handleInputChange('base_url', value);
                 }}
                 value={inputs.base_url}
-                autoComplete="new-password"
+                autoComplete='new-password'
               />
               <div style={{ marginTop: 10 }}>
                 <Typography.Text strong>{t('默认 API 版本')}:</Typography.Text>
               </div>
               <Input
                 label={t('默认 API 版本')}
-                name="azure_other"
+                name='azure_other'
                 placeholder={t('请输入默认 API 版本,例如:2024-12-01-preview')}
                 onChange={(value) => {
                   handleInputChange('other', value);
                 }}
                 value={inputs.other}
-                autoComplete="new-password"
+                autoComplete='new-password'
               />
             </>
           )}
@@ -511,7 +512,9 @@ const EditChannel = (props) => {
               <div style={{ marginTop: 10 }}>
                 <Banner
                   type={'warning'}
-                  description={t('如果你对接的是上游One API或者New API等转发项目,请使用OpenAI类型,不要使用此类型,除非你知道你在做什么。')}
+                  description={t(
+                    '如果你对接的是上游One API或者New API等转发项目,请使用OpenAI类型,不要使用此类型,除非你知道你在做什么。',
+                  )}
                 ></Banner>
               </div>
               <div style={{ marginTop: 10 }}>
@@ -520,13 +523,15 @@ const EditChannel = (props) => {
                 </Typography.Text>
               </div>
               <Input
-                name="base_url"
-                placeholder={t('请输入完整的URL,例如:https://api.openai.com/v1/chat/completions')}
+                name='base_url'
+                placeholder={t(
+                  '请输入完整的URL,例如:https://api.openai.com/v1/chat/completions',
+                )}
                 onChange={(value) => {
                   handleInputChange('base_url', value);
                 }}
                 value={inputs.base_url}
-                autoComplete="new-password"
+                autoComplete='new-password'
               />
             </>
           )}
@@ -535,7 +540,9 @@ const EditChannel = (props) => {
               <div style={{ marginTop: 10 }}>
                 <Banner
                   type={'warning'}
-                  description={t('Dify渠道只适配chatflow和agent,并且agent不支持图片!')}
+                  description={t(
+                    'Dify渠道只适配chatflow和agent,并且agent不支持图片!',
+                  )}
                 ></Banner>
               </div>
             </>
@@ -545,13 +552,13 @@ const EditChannel = (props) => {
           </div>
           <Input
             required
-            name="name"
+            name='name'
             placeholder={t('请为渠道命名')}
             onChange={(value) => {
               handleInputChange('name', value);
             }}
             value={inputs.name}
-            autoComplete="new-password"
+            autoComplete='new-password'
           />
           {inputs.type !== 3 && inputs.type !== 8 && inputs.type !== 22 && inputs.type !== 36 && inputs.type !== 45 && (
             <>
@@ -578,7 +585,7 @@ const EditChannel = (props) => {
           {batch ? (
             <TextArea
               label={t('密钥')}
-              name="key"
+              name='key'
               required
               placeholder={t('请输入密钥,一行一个')}
               onChange={(value) => {
@@ -586,16 +593,17 @@ const EditChannel = (props) => {
               }}
               value={inputs.key}
               style={{ minHeight: 150, fontFamily: 'JetBrains Mono, Consolas' }}
-              autoComplete="new-password"
+              autoComplete='new-password'
             />
           ) : (
             <>
               {inputs.type === 41 ? (
                 <TextArea
                   label={t('鉴权json')}
-                  name="key"
+                  name='key'
                   required
-                  placeholder={'{\n' +
+                  placeholder={
+                    '{\n' +
                     '  "type": "service_account",\n' +
                     '  "project_id": "abc-bcd-123-456",\n' +
                     '  "private_key_id": "123xxxxx456",\n' +
@@ -607,25 +615,26 @@ const EditChannel = (props) => {
                     '  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",\n' +
                     '  "client_x509_cert_url": "https://xxxxx.gserviceaccount.com",\n' +
                     '  "universe_domain": "googleapis.com"\n' +
-                    '}'}
+                    '}'
+                  }
                   onChange={(value) => {
                     handleInputChange('key', value);
                   }}
                   autosize={{ minRows: 10 }}
                   value={inputs.key}
-                  autoComplete="new-password"
+                  autoComplete='new-password'
                 />
               ) : (
                 <Input
                   label={t('密钥')}
-                  name="key"
+                  name='key'
                   required
                   placeholder={t(type2secretPrompt(inputs.type))}
                   onChange={(value) => {
                     handleInputChange('key', value);
                   }}
                   value={inputs.key}
-                  autoComplete="new-password"
+                  autoComplete='new-password'
                 />
               )}
             </>
@@ -636,7 +645,7 @@ const EditChannel = (props) => {
                 <Checkbox
                   checked={batch}
                   label={t('批量创建')}
-                  name="batch"
+                  name='batch'
                   onChange={() => setBatch(!batch)}
                 />
                 <Typography.Text strong>{t('批量创建')}</Typography.Text>
@@ -649,13 +658,15 @@ const EditChannel = (props) => {
                 <Typography.Text strong>{t('私有部署地址')}:</Typography.Text>
               </div>
               <Input
-                name="base_url"
-                placeholder={t('请输入私有部署地址,格式为:https://fastgpt.run/api/openapi')}
+                name='base_url'
+                placeholder={t(
+                  '请输入私有部署地址,格式为:https://fastgpt.run/api/openapi',
+                )}
                 onChange={(value) => {
                   handleInputChange('base_url', value);
                 }}
                 value={inputs.base_url}
-                autoComplete="new-password"
+                autoComplete='new-password'
               />
             </>
           )}
@@ -663,17 +674,21 @@ const EditChannel = (props) => {
             <>
               <div style={{ marginTop: 10 }}>
                 <Typography.Text strong>
-                  {t('注意非Chat API,请务必填写正确的API地址,否则可能导致无法使用')}
+                  {t(
+                    '注意非Chat API,请务必填写正确的API地址,否则可能导致无法使用',
+                  )}
                 </Typography.Text>
               </div>
               <Input
-                name="base_url"
-                placeholder={t('请输入到 /suno 前的路径,通常就是域名,例如:https://api.example.com')}
+                name='base_url'
+                placeholder={t(
+                  '请输入到 /suno 前的路径,通常就是域名,例如:https://api.example.com',
+                )}
                 onChange={(value) => {
                   handleInputChange('base_url', value);
                 }}
                 value={inputs.base_url}
-                autoComplete="new-password"
+                autoComplete='new-password'
               />
             </>
           )}
@@ -682,7 +697,7 @@ const EditChannel = (props) => {
           </div>
           <Select
             placeholder={t('请选择可以使用该渠道的分组')}
-            name="groups"
+            name='groups'
             required
             multiple
             selection
@@ -692,7 +707,7 @@ const EditChannel = (props) => {
               handleInputChange('groups', value);
             }}
             value={inputs.groups}
-            autoComplete="new-password"
+            autoComplete='new-password'
             optionList={groupOptions}
           />
           {inputs.type === 18 && (
@@ -701,7 +716,7 @@ const EditChannel = (props) => {
                 <Typography.Text strong>模型版本:</Typography.Text>
               </div>
               <Input
-                name="other"
+                name='other'
                 placeholder={
                   '请输入星火大模型版本,注意是接口地址中的版本号,例如:v2.1'
                 }
@@ -709,7 +724,7 @@ const EditChannel = (props) => {
                   handleInputChange('other', value);
                 }}
                 value={inputs.other}
-                autoComplete="new-password"
+                autoComplete='new-password'
               />
             </>
           )}
@@ -719,29 +734,31 @@ const EditChannel = (props) => {
                 <Typography.Text strong>{t('部署地区')}:</Typography.Text>
               </div>
               <TextArea
-                name="other"
-                placeholder={t('请输入部署地区,例如:us-central1\n支持使用模型映射格式\n' +
+                name='other'
+                placeholder={t(
+                  '请输入部署地区,例如:us-central1\n支持使用模型映射格式\n' +
                   '{\n' +
                   '    "default": "us-central1",\n' +
                   '    "claude-3-5-sonnet-20240620": "europe-west1"\n' +
-                  '}')}
+                  '}',
+                )}
                 autosize={{ minRows: 2 }}
                 onChange={(value) => {
                   handleInputChange('other', value);
                 }}
                 value={inputs.other}
-                autoComplete="new-password"
+                autoComplete='new-password'
               />
               <Typography.Text
                 style={{
                   color: 'rgba(var(--semi-blue-5), 1)',
                   userSelect: 'none',
-                  cursor: 'pointer'
+                  cursor: 'pointer',
                 }}
                 onClick={() => {
                   handleInputChange(
                     'other',
-                    JSON.stringify(REGION_EXAMPLE, null, 2)
+                    JSON.stringify(REGION_EXAMPLE, null, 2),
                   );
                 }}
               >
@@ -755,14 +772,14 @@ const EditChannel = (props) => {
                 <Typography.Text strong>知识库 ID:</Typography.Text>
               </div>
               <Input
-                label="知识库 ID"
-                name="other"
+                label='知识库 ID'
+                name='other'
                 placeholder={'请输入知识库 ID,例如:123456'}
                 onChange={(value) => {
                   handleInputChange('other', value);
                 }}
                 value={inputs.other}
-                autoComplete="new-password"
+                autoComplete='new-password'
               />
             </>
           )}
@@ -772,7 +789,7 @@ const EditChannel = (props) => {
                 <Typography.Text strong>Account ID:</Typography.Text>
               </div>
               <Input
-                name="other"
+                name='other'
                 placeholder={
                   '请输入Account ID,例如:d6b5da8hk1awo8nap34ube6gh'
                 }
@@ -780,7 +797,7 @@ const EditChannel = (props) => {
                   handleInputChange('other', value);
                 }}
                 value={inputs.other}
-                autoComplete="new-password"
+                autoComplete='new-password'
               />
             </>
           )}
@@ -789,7 +806,7 @@ const EditChannel = (props) => {
           </div>
           <Select
             placeholder={'请选择该渠道所支持的模型'}
-            name="models"
+            name='models'
             required
             multiple
             selection
@@ -799,13 +816,13 @@ const EditChannel = (props) => {
               handleInputChange('models', value);
             }}
             value={inputs.models}
-            autoComplete="new-password"
+            autoComplete='new-password'
             optionList={modelOptions}
           />
           <div style={{ lineHeight: '40px', marginBottom: '12px' }}>
             <Space>
               <Button
-                type="primary"
+                type='primary'
                 onClick={() => {
                   handleInputChange('models', basicModels);
                 }}
@@ -813,16 +830,20 @@ const EditChannel = (props) => {
                 {t('填入相关模型')}
               </Button>
               <Button
-                type="secondary"
+                type='secondary'
                 onClick={() => {
                   handleInputChange('models', fullModels);
                 }}
               >
                 {t('填入所有模型')}
               </Button>
-              <Tooltip content={t('新建渠道时,请求通过当前浏览器发出;编辑已有渠道,请求通过后端服务器发出')}>
+              <Tooltip
+                content={t(
+                  '新建渠道时,请求通过当前浏览器发出;编辑已有渠道,请求通过后端服务器发出',
+                )}
+              >
                 <Button
-                  type="tertiary"
+                  type='tertiary'
                   onClick={() => {
                     fetchUpstreamModelList('models');
                   }}
@@ -831,7 +852,7 @@ const EditChannel = (props) => {
                 </Button>
               </Tooltip>
               <Button
-                type="warning"
+                type='warning'
                 onClick={() => {
                   handleInputChange('models', []);
                 }}
@@ -841,7 +862,7 @@ const EditChannel = (props) => {
             </Space>
             <Input
               addonAfter={
-                <Button type="primary" onClick={addCustomModels}>
+                <Button type='primary' onClick={addCustomModels}>
                   {t('填入')}
                 </Button>
               }
@@ -856,53 +877,53 @@ const EditChannel = (props) => {
             <Typography.Text strong>{t('模型重定向')}:</Typography.Text>
           </div>
           <TextArea
-            placeholder={t('此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,例如:') + `\n${JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)}`}
-            name="model_mapping"
+            placeholder={
+              t(
+                '此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,例如:',
+              ) + `\n${JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)}`
+            }
+            name='model_mapping'
             onChange={(value) => {
               handleInputChange('model_mapping', value);
             }}
             autosize
             value={inputs.model_mapping}
-            autoComplete="new-password"
+            autoComplete='new-password'
           />
           <Typography.Text
             style={{
               color: 'rgba(var(--semi-blue-5), 1)',
               userSelect: 'none',
-              cursor: 'pointer'
+              cursor: 'pointer',
             }}
             onClick={() => {
               handleInputChange(
                 'model_mapping',
-                JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)
+                JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2),
               );
             }}
           >
             {t('填入模板')}
           </Typography.Text>
           <div style={{ marginTop: 10 }}>
-            <Typography.Text strong>
-              {t('渠道标签')}
-            </Typography.Text>
+            <Typography.Text strong>{t('渠道标签')}</Typography.Text>
           </div>
           <Input
             label={t('渠道标签')}
-            name="tag"
+            name='tag'
             placeholder={t('渠道标签')}
             onChange={(value) => {
               handleInputChange('tag', value);
             }}
             value={inputs.tag}
-            autoComplete="new-password"
+            autoComplete='new-password'
           />
           <div style={{ marginTop: 10 }}>
-            <Typography.Text strong>
-              {t('渠道优先级')}
-            </Typography.Text>
+            <Typography.Text strong>{t('渠道优先级')}</Typography.Text>
           </div>
           <Input
             label={t('渠道优先级')}
-            name="priority"
+            name='priority'
             placeholder={t('渠道优先级')}
             onChange={(value) => {
               const number = parseInt(value);
@@ -913,16 +934,14 @@ const EditChannel = (props) => {
               }
             }}
             value={inputs.priority}
-            autoComplete="new-password"
+            autoComplete='new-password'
           />
           <div style={{ marginTop: 10 }}>
-            <Typography.Text strong>
-              {t('渠道权重')}
-            </Typography.Text>
+            <Typography.Text strong>{t('渠道权重')}</Typography.Text>
           </div>
           <Input
             label={t('渠道权重')}
-            name="weight"
+            name='weight'
             placeholder={t('渠道权重')}
             onChange={(value) => {
               const number = parseInt(value);
@@ -933,37 +952,43 @@ const EditChannel = (props) => {
               }
             }}
             value={inputs.weight}
-            autoComplete="new-password"
+            autoComplete='new-password'
           />
           <>
             <div style={{ marginTop: 10 }}>
-              <Typography.Text strong>
-                {t('渠道额外设置')}:
-              </Typography.Text>
+              <Typography.Text strong>{t('渠道额外设置')}:</Typography.Text>
             </div>
             <TextArea
-              placeholder={t('此项可选,用于配置渠道特定设置,为一个 JSON 字符串,例如:') + '\n{\n  "force_format": true\n}'}
-              name="setting"
+              placeholder={
+                t(
+                  '此项可选,用于配置渠道特定设置,为一个 JSON 字符串,例如:',
+                ) + '\n{\n  "force_format": true\n}'
+              }
+              name='setting'
               onChange={(value) => {
                 handleInputChange('setting', value);
               }}
               autosize
               value={inputs.setting}
-              autoComplete="new-password"
+              autoComplete='new-password'
             />
             <Space>
               <Typography.Text
                 style={{
                   color: 'rgba(var(--semi-blue-5), 1)',
                   userSelect: 'none',
-                  cursor: 'pointer'
+                  cursor: 'pointer',
                 }}
                 onClick={() => {
                   handleInputChange(
                     'setting',
-                    JSON.stringify({
-                      force_format: true
-                    }, null, 2)
+                    JSON.stringify(
+                      {
+                        force_format: true,
+                      },
+                      null,
+                      2,
+                    ),
                   );
                 }}
               >
@@ -973,10 +998,12 @@ const EditChannel = (props) => {
                 style={{
                   color: 'rgba(var(--semi-blue-5), 1)',
                   userSelect: 'none',
-                  cursor: 'pointer'
+                  cursor: 'pointer',
                 }}
                 onClick={() => {
-                  window.open('https://github.com/Calcium-Ion/new-api/blob/main/docs/channel/other_setting.md');
+                  window.open(
+                    'https://github.com/Calcium-Ion/new-api/blob/main/docs/channel/other_setting.md',
+                  );
                 }}
               >
                 {t('设置说明')}
@@ -985,19 +1012,21 @@ const EditChannel = (props) => {
           </>
           <>
             <div style={{ marginTop: 10 }}>
-              <Typography.Text strong>
-                {t('参数覆盖')}:
-              </Typography.Text>
+              <Typography.Text strong>{t('参数覆盖')}:</Typography.Text>
             </div>
             <TextArea
-              placeholder={t('此项可选,用于覆盖请求参数。不支持覆盖 stream 参数。为一个 JSON 字符串,例如:') + '\n{\n  "temperature": 0\n}'}
-              name="setting"
+              placeholder={
+                t(
+                  '此项可选,用于覆盖请求参数。不支持覆盖 stream 参数。为一个 JSON 字符串,例如:',
+                ) + '\n{\n  "temperature": 0\n}'
+              }
+              name='setting'
               onChange={(value) => {
                 handleInputChange('param_override', value);
               }}
               autosize
               value={inputs.param_override}
-              autoComplete="new-password"
+              autoComplete='new-password'
             />
           </>
           {inputs.type === 1 && (
@@ -1007,7 +1036,7 @@ const EditChannel = (props) => {
               </div>
               <Input
                 label={t('组织,可选,不填则为默认组织')}
-                name="openai_organization"
+                name='openai_organization'
                 placeholder={t('请输入组织org-xxx')}
                 onChange={(value) => {
                   handleInputChange('openai_organization', value);
@@ -1020,7 +1049,7 @@ const EditChannel = (props) => {
             <Typography.Text strong>{t('默认测试模型')}:</Typography.Text>
           </div>
           <Input
-            name="test_model"
+            name='test_model'
             placeholder={t('不填则为模型列表第一个')}
             onChange={(value) => {
               handleInputChange('test_model', value);
@@ -1030,14 +1059,16 @@ const EditChannel = (props) => {
           <div style={{ marginTop: 10, display: 'flex' }}>
             <Space>
               <Checkbox
-                name="auto_ban"
+                name='auto_ban'
                 checked={autoBan}
                 onChange={() => {
                   setAutoBan(!autoBan);
                 }}
               />
               <Typography.Text strong>
-                {t('是否自动禁用(仅当自动禁用开启时有效),关闭后不会自动禁用该渠道:')}
+                {t(
+                  '是否自动禁用(仅当自动禁用开启时有效),关闭后不会自动禁用该渠道:',
+                )}
               </Typography.Text>
             </Space>
           </div>
@@ -1047,26 +1078,31 @@ const EditChannel = (props) => {
             </Typography.Text>
           </div>
           <TextArea
-            placeholder={t('此项可选,用于复写返回的状态码,比如将claude渠道的400错误复写为500(用于重试),请勿滥用该功能,例如:') +
-              '\n' + JSON.stringify(STATUS_CODE_MAPPING_EXAMPLE, null, 2)}
-            name="status_code_mapping"
+            placeholder={
+              t(
+                '此项可选,用于复写返回的状态码,比如将claude渠道的400错误复写为500(用于重试),请勿滥用该功能,例如:',
+              ) +
+              '\n' +
+              JSON.stringify(STATUS_CODE_MAPPING_EXAMPLE, null, 2)
+            }
+            name='status_code_mapping'
             onChange={(value) => {
               handleInputChange('status_code_mapping', value);
             }}
             autosize
             value={inputs.status_code_mapping}
-            autoComplete="new-password"
+            autoComplete='new-password'
           />
           <Typography.Text
             style={{
               color: 'rgba(var(--semi-blue-5), 1)',
               userSelect: 'none',
-              cursor: 'pointer'
+              cursor: 'pointer',
             }}
             onClick={() => {
               handleInputChange(
                 'status_code_mapping',
-                JSON.stringify(STATUS_CODE_MAPPING_EXAMPLE, null, 2)
+                JSON.stringify(STATUS_CODE_MAPPING_EXAMPLE, null, 2),
               );
             }}
           >

+ 64 - 54
web/src/pages/Channel/EditTagModal.js

@@ -1,11 +1,29 @@
 import React, { useState, useEffect } from 'react';
-import { API, showError, showInfo, showSuccess, showWarning, verifyJSON } from '../../helpers';
-import { SideSheet, Space, Button, Input, Typography, Spin, Modal, Select, Banner, TextArea } from '@douyinfe/semi-ui';
+import {
+  API,
+  showError,
+  showInfo,
+  showSuccess,
+  showWarning,
+  verifyJSON,
+} from '../../helpers';
+import {
+  SideSheet,
+  Space,
+  Button,
+  Input,
+  Typography,
+  Spin,
+  Modal,
+  Select,
+  Banner,
+  TextArea,
+} from '@douyinfe/semi-ui';
 import TextInput from '../../components/custom/TextInput.js';
 import { getChannelModels } from '../../components/utils.js';
 
 const MODEL_MAPPING_EXAMPLE = {
-  'gpt-3.5-turbo': 'gpt-3.5-turbo-0125'
+  'gpt-3.5-turbo': 'gpt-3.5-turbo-0125',
 };
 
 const EditTagModal = (props) => {
@@ -23,7 +41,7 @@ const EditTagModal = (props) => {
     model_mapping: null,
     groups: [],
     models: [],
-  }
+  };
   const [inputs, setInputs] = useState(originInputs);
 
   const handleInputChange = (name, value) => {
@@ -39,7 +57,7 @@ const EditTagModal = (props) => {
             'mj_blend',
             'mj_upscale',
             'mj_describe',
-            'mj_uploads'
+            'mj_uploads',
           ];
           break;
         case 5:
@@ -59,14 +77,11 @@ const EditTagModal = (props) => {
             'mj_high_variation',
             'mj_low_variation',
             'mj_pan',
-            'mj_uploads'
+            'mj_uploads',
           ];
           break;
         case 36:
-          localModels = [
-            'suno_music',
-            'suno_lyrics'
-          ];
+          localModels = ['suno_music', 'suno_lyrics'];
           break;
         default:
           localModels = getChannelModels(value);
@@ -84,7 +99,7 @@ const EditTagModal = (props) => {
       let res = await API.get(`/api/channel/models`);
       let localModelOptions = res.data.data.map((model) => ({
         label: model.id,
-        value: model.id
+        value: model.id,
       }));
       setOriginModelOptions(localModelOptions);
       setFullModels(res.data.data.map((model) => model.id));
@@ -93,7 +108,7 @@ const EditTagModal = (props) => {
           .filter((model) => {
             return model.id.startsWith('gpt-') || model.id.startsWith('text-');
           })
-          .map((model) => model.id)
+          .map((model) => model.id),
       );
     } catch (error) {
       showError(error.message);
@@ -109,27 +124,26 @@ const EditTagModal = (props) => {
       setGroupOptions(
         res.data.data.map((group) => ({
           label: group,
-          value: group
-        }))
+          value: group,
+        })),
       );
     } catch (error) {
       showError(error.message);
     }
   };
 
-
   const handleSave = async () => {
     setLoading(true);
     let data = {
       tag: tag,
-    }
+    };
     if (inputs.model_mapping !== null && inputs.model_mapping !== '') {
       if (inputs.model_mapping !== '' && !verifyJSON(inputs.model_mapping)) {
         showInfo('模型映射必须是合法的 JSON 格式!');
         setLoading(false);
         return;
       }
-      data.model_mapping = inputs.model_mapping
+      data.model_mapping = inputs.model_mapping;
     }
     if (inputs.groups.length > 0) {
       data.groups = inputs.groups.join(',');
@@ -139,7 +153,12 @@ const EditTagModal = (props) => {
     }
     data.new_tag = inputs.new_tag;
     // check have any change
-    if (data.model_mapping === undefined && data.groups === undefined && data.models === undefined && data.new_tag === undefined) {
+    if (
+      data.model_mapping === undefined &&
+      data.groups === undefined &&
+      data.models === undefined &&
+      data.new_tag === undefined
+    ) {
       showWarning('没有任何修改!');
       setLoading(false);
       return;
@@ -159,7 +178,7 @@ const EditTagModal = (props) => {
     } catch (error) {
       showError(error);
     }
-  }
+  };
 
   useEffect(() => {
     let localModelOptions = [...originModelOptions];
@@ -167,7 +186,7 @@ const EditTagModal = (props) => {
       if (!localModelOptions.find((option) => option.label === model)) {
         localModelOptions.push({
           label: model,
-          value: model
+          value: model,
         });
       }
     });
@@ -179,7 +198,7 @@ const EditTagModal = (props) => {
       ...originInputs,
       tag: tag,
       new_tag: tag,
-    })
+    });
     fetchModels().then();
     fetchGroups().then();
   }, [visible]);
@@ -201,7 +220,7 @@ const EditTagModal = (props) => {
           // 添加到下拉选项
           key: model,
           text: model,
-          value: model
+          value: model,
         });
       } else if (model) {
         showError('某些模型已存在!');
@@ -217,17 +236,18 @@ const EditTagModal = (props) => {
     handleInputChange('models', localModels);
   };
 
-
   return (
     <SideSheet
-      title="编辑标签"
+      title='编辑标签'
       visible={visible}
       onCancel={handleClose}
       footer={
         <div style={{ display: 'flex', justifyContent: 'flex-end' }}>
           <Space>
             <Button onClick={handleClose}>取消</Button>
-            <Button type="primary" onClick={handleSave} loading={loading}>保存</Button>
+            <Button type='primary' onClick={handleSave} loading={loading}>
+              保存
+            </Button>
           </Space>
         </div>
       }
@@ -235,27 +255,23 @@ const EditTagModal = (props) => {
       <div style={{ marginTop: 10 }}>
         <Banner
           type={'warning'}
-          description={
-            <>
-              所有编辑均为覆盖操作,留空则不更改
-            </>
-          }
+          description={<>所有编辑均为覆盖操作,留空则不更改</>}
         ></Banner>
       </div>
       <Spin spinning={loading}>
         <TextInput
-          label="标签名,留空则解散标签"
-          name="newTag"
+          label='标签名,留空则解散标签'
+          name='newTag'
           value={inputs.new_tag}
           onChange={(value) => setInputs({ ...inputs, new_tag: value })}
-          placeholder="请输入新标签"
+          placeholder='请输入新标签'
         />
         <div style={{ marginTop: 10 }}>
           <Typography.Text strong>模型,留空则不更改:</Typography.Text>
         </div>
         <Select
           placeholder={'请选择该渠道所支持的模型,留空则不更改'}
-          name="models"
+          name='models'
           required
           multiple
           selection
@@ -265,16 +281,16 @@ const EditTagModal = (props) => {
             handleInputChange('models', value);
           }}
           value={inputs.models}
-          autoComplete="new-password"
+          autoComplete='new-password'
           optionList={modelOptions}
         />
         <Input
           addonAfter={
-            <Button type="primary" onClick={addCustomModels}>
+            <Button type='primary' onClick={addCustomModels}>
               填入
             </Button>
           }
-          placeholder="输入自定义模型名称"
+          placeholder='输入自定义模型名称'
           value={customModel}
           onChange={(value) => {
             setCustomModel(value.trim());
@@ -285,7 +301,7 @@ const EditTagModal = (props) => {
         </div>
         <Select
           placeholder={'请选择可以使用该渠道的分组,留空则不更改'}
-          name="groups"
+          name='groups'
           required
           multiple
           selection
@@ -295,7 +311,7 @@ const EditTagModal = (props) => {
             handleInputChange('groups', value);
           }}
           value={inputs.groups}
-          autoComplete="new-password"
+          autoComplete='new-password'
           optionList={groupOptions}
         />
         <div style={{ marginTop: 10 }}>
@@ -303,25 +319,25 @@ const EditTagModal = (props) => {
         </div>
         <TextArea
           placeholder={`此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,留空则不更改`}
-          name="model_mapping"
+          name='model_mapping'
           onChange={(value) => {
             handleInputChange('model_mapping', value);
           }}
           autosize
           value={inputs.model_mapping}
-          autoComplete="new-password"
+          autoComplete='new-password'
         />
         <Space>
           <Typography.Text
             style={{
               color: 'rgba(var(--semi-blue-5), 1)',
               userSelect: 'none',
-              cursor: 'pointer'
+              cursor: 'pointer',
             }}
             onClick={() => {
               handleInputChange(
                 'model_mapping',
-                JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)
+                JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2),
               );
             }}
           >
@@ -331,13 +347,10 @@ const EditTagModal = (props) => {
             style={{
               color: 'rgba(var(--semi-blue-5), 1)',
               userSelect: 'none',
-              cursor: 'pointer'
+              cursor: 'pointer',
             }}
             onClick={() => {
-              handleInputChange(
-                'model_mapping',
-                JSON.stringify({}, null, 2)
-              );
+              handleInputChange('model_mapping', JSON.stringify({}, null, 2));
             }}
           >
             清空重定向
@@ -346,13 +359,10 @@ const EditTagModal = (props) => {
             style={{
               color: 'rgba(var(--semi-blue-5), 1)',
               userSelect: 'none',
-              cursor: 'pointer'
+              cursor: 'pointer',
             }}
             onClick={() => {
-              handleInputChange(
-                'model_mapping',
-                ""
-              );
+              handleInputChange('model_mapping', '');
             }}
           >
             不更改
@@ -363,4 +373,4 @@ const EditTagModal = (props) => {
   );
 };
 
-export default EditTagModal;
+export default EditTagModal;

+ 4 - 4
web/src/pages/Channel/index.js

@@ -9,10 +9,10 @@ const File = () => {
     <>
       <Layout>
         <Layout.Header>
-        <h3>{t('管理渠道')}</h3>
-      </Layout.Header>
-      <Layout.Content>
-        <ChannelsTable />
+          <h3>{t('管理渠道')}</h3>
+        </Layout.Header>
+        <Layout.Content>
+          <ChannelsTable />
         </Layout.Content>
       </Layout>
     </>

+ 22 - 22
web/src/pages/Chat/index.js

@@ -1,6 +1,6 @@
-import React, {useEffect} from 'react';
+import React, { useEffect } from 'react';
 import { useTokenKeys } from '../../components/fetchTokenKeys';
-import {Banner, Layout} from '@douyinfe/semi-ui';
+import { Banner, Layout } from '@douyinfe/semi-ui';
 import { useParams } from 'react-router-dom';
 
 const ChatPage = () => {
@@ -10,21 +10,24 @@ const ChatPage = () => {
   const comLink = (key) => {
     // console.log('chatLink:', chatLink);
     if (!serverAddress || !key) return '';
-      let link = "";
-      if (id) {
-          let chats = localStorage.getItem('chats');
-          if (chats) {
-              chats = JSON.parse(chats);
-              if (Array.isArray(chats) && chats.length > 0) {
-                  for (let k in chats[id]) {
-                      link = chats[id][k];
-                      link = link.replaceAll('{address}', encodeURIComponent(serverAddress));
-                      link = link.replaceAll('{key}', 'sk-' + key);
-                  }
-              }
+    let link = '';
+    if (id) {
+      let chats = localStorage.getItem('chats');
+      if (chats) {
+        chats = JSON.parse(chats);
+        if (Array.isArray(chats) && chats.length > 0) {
+          for (let k in chats[id]) {
+            link = chats[id][k];
+            link = link.replaceAll(
+              '{address}',
+              encodeURIComponent(serverAddress),
+            );
+            link = link.replaceAll('{key}', 'sk-' + key);
           }
+        }
       }
-      return link;
+    }
+    return link;
   };
 
   const iframeSrc = keys.length > 0 ? comLink(keys[0]) : '';
@@ -33,21 +36,18 @@ const ChatPage = () => {
     <iframe
       src={iframeSrc}
       style={{ width: '100%', height: '100%', border: 'none' }}
-      title="Token Frame"
-      allow="camera;microphone"
+      title='Token Frame'
+      allow='camera;microphone'
     />
   ) : (
     <div>
       <Layout>
         <Layout.Header>
-          <Banner
-              description={"正在跳转......"}
-              type={"warning"}
-          />
+          <Banner description={'正在跳转......'} type={'warning'} />
         </Layout.Header>
       </Layout>
     </div>
   );
 };
 
-export default ChatPage;
+export default ChatPage;

+ 2 - 2
web/src/pages/Chat2Link/index.js

@@ -18,9 +18,9 @@ const chat2page = () => {
 
   return (
     <div>
-        <h3>正在加载,请稍候...</h3>
+      <h3>正在加载,请稍候...</h3>
     </div>
   );
 };
 
-export default chat2page;
+export default chat2page;

+ 92 - 65
web/src/pages/Detail/index.js

@@ -1,8 +1,18 @@
 import React, { useContext, useEffect, useRef, useState } from 'react';
 import { initVChartSemiTheme } from '@visactor/vchart-semi-theme';
 
-import { Button, Card, Col, Descriptions, Form, Layout, Row, Spin, Tabs } from '@douyinfe/semi-ui';
-import { VChart } from "@visactor/react-vchart";
+import {
+  Button,
+  Card,
+  Col,
+  Descriptions,
+  Form,
+  Layout,
+  Row,
+  Spin,
+  Tabs,
+} from '@douyinfe/semi-ui';
+import { VChart } from '@visactor/react-vchart';
 import {
   API,
   isAdmin,
@@ -59,10 +69,12 @@ const Detail = (props) => {
   const [lineData, setLineData] = useState([]);
   const [spec_pie, setSpecPie] = useState({
     type: 'pie',
-    data: [{
-      id: 'id0',
-      values: pieData
-    }],
+    data: [
+      {
+        id: 'id0',
+        values: pieData,
+      },
+    ],
     outerRadius: 0.8,
     innerRadius: 0.5,
     padAngle: 0.6,
@@ -113,10 +125,12 @@ const Detail = (props) => {
   });
   const [spec_line, setSpecLine] = useState({
     type: 'bar',
-    data: [{
-      id: 'barData',
-      values: lineData
-    }],
+    data: [
+      {
+        id: 'barData',
+        values: lineData,
+      },
+    ],
     xField: 'Time',
     yField: 'Usage',
     seriesField: 'Model',
@@ -158,7 +172,7 @@ const Detail = (props) => {
           array.sort((a, b) => b.value - a.value);
           let sum = 0;
           for (let i = 0; i < array.length; i++) {
-            if (array[i].key == "其他") {
+            if (array[i].key == '其他') {
               continue;
             }
             let value = parseFloat(array[i].value);
@@ -245,7 +259,7 @@ const Detail = (props) => {
     let totalTokens = 0;
 
     // 收集所有唯一的模型名称
-    data.forEach(item => {
+    data.forEach((item) => {
       uniqueModels.add(item.model_name);
       totalTokens += item.token_used;
       totalQuota += item.quota;
@@ -255,15 +269,16 @@ const Detail = (props) => {
     // 处理颜色映射
     const newModelColors = {};
     Array.from(uniqueModels).forEach((modelName) => {
-      newModelColors[modelName] = modelColorMap[modelName] || 
-        modelColors[modelName] || 
+      newModelColors[modelName] =
+        modelColorMap[modelName] ||
+        modelColors[modelName] ||
         modelToColor(modelName);
     });
     setModelColors(newModelColors);
 
     // 按时间和模型聚合数据
     let aggregatedData = new Map();
-    data.forEach(item => {
+    data.forEach((item) => {
       const timeKey = timestamp2string1(item.created_at, dataExportDefaultTime);
       const modelKey = item.model_name;
       const key = `${timeKey}-${modelKey}`;
@@ -273,10 +288,10 @@ const Detail = (props) => {
           time: timeKey,
           model: modelKey,
           quota: 0,
-          count: 0
+          count: 0,
         });
       }
-      
+
       const existing = aggregatedData.get(key);
       existing.quota += item.quota;
       existing.count += item.count;
@@ -293,48 +308,53 @@ const Detail = (props) => {
 
     newPieData = Array.from(modelTotals).map(([model, count]) => ({
       type: model,
-      value: count
+      value: count,
     }));
 
     // 生成时间点序列
-    let timePoints = Array.from(new Set([...aggregatedData.values()].map(d => d.time)));
+    let timePoints = Array.from(
+      new Set([...aggregatedData.values()].map((d) => d.time)),
+    );
     if (timePoints.length < 7) {
-      const lastTime = Math.max(...data.map(item => item.created_at));
-      const interval = dataExportDefaultTime === 'hour' ? 3600 
-                      : dataExportDefaultTime === 'day' ? 86400 
-                      : 604800;
-      
-      timePoints = Array.from({length: 7}, (_, i) => 
-        timestamp2string1(lastTime - (6-i) * interval, dataExportDefaultTime)
+      const lastTime = Math.max(...data.map((item) => item.created_at));
+      const interval =
+        dataExportDefaultTime === 'hour'
+          ? 3600
+          : dataExportDefaultTime === 'day'
+            ? 86400
+            : 604800;
+
+      timePoints = Array.from({ length: 7 }, (_, i) =>
+        timestamp2string1(lastTime - (6 - i) * interval, dataExportDefaultTime),
       );
     }
 
     // 生成柱状图数据
-    timePoints.forEach(time => {
+    timePoints.forEach((time) => {
       // 为每个时间点收集所有模型的数据
-      let timeData = Array.from(uniqueModels).map(model => {
+      let timeData = Array.from(uniqueModels).map((model) => {
         const key = `${time}-${model}`;
         const aggregated = aggregatedData.get(key);
         return {
           Time: time,
           Model: model,
           rawQuota: aggregated?.quota || 0,
-          Usage: aggregated?.quota ? getQuotaWithUnit(aggregated.quota, 4) : 0
+          Usage: aggregated?.quota ? getQuotaWithUnit(aggregated.quota, 4) : 0,
         };
       });
-      
+
       // 计算该时间点的总计
       const timeSum = timeData.reduce((sum, item) => sum + item.rawQuota, 0);
-      
+
       // 按照 rawQuota 从大到小排序
       timeData.sort((a, b) => b.rawQuota - a.rawQuota);
-      
+
       // 为每个数据点添加该时间的总计
-      timeData = timeData.map(item => ({
+      timeData = timeData.map((item) => ({
         ...item,
-        TimeSum: timeSum
+        TimeSum: timeSum,
       }));
-      
+
       // 将排序后的数据添加到 newLineData
       newLineData.push(...timeData);
     });
@@ -344,30 +364,30 @@ const Detail = (props) => {
     newLineData.sort((a, b) => a.Time.localeCompare(b.Time));
 
     // 更新图表配置和数据
-    setSpecPie(prev => ({
+    setSpecPie((prev) => ({
       ...prev,
       data: [{ id: 'id0', values: newPieData }],
       title: {
         ...prev.title,
-        subtext: `${t('总计')}:${renderNumber(totalTimes)}`
+        subtext: `${t('总计')}:${renderNumber(totalTimes)}`,
       },
       color: {
-        specified: newModelColors
-      }
+        specified: newModelColors,
+      },
     }));
 
-    setSpecLine(prev => ({
+    setSpecLine((prev) => ({
       ...prev,
       data: [{ id: 'barData', values: newLineData }],
       title: {
         ...prev.title,
-        subtext: `${t('总计')}:${renderQuota(totalQuota, 2)}`
+        subtext: `${t('总计')}:${renderQuota(totalQuota, 2)}`,
       },
       color: {
-        specified: newModelColors
-      }
+        specified: newModelColors,
+      },
     }));
-    
+
     setPieData(newPieData);
     setLineData(newLineData);
     setConsumeQuota(totalQuota);
@@ -377,16 +397,16 @@ const Detail = (props) => {
 
   const getUserData = async () => {
     let res = await API.get(`/api/user/self`);
-    const {success, message, data} = res.data;
+    const { success, message, data } = res.data;
     if (success) {
-      userDispatch({type: 'login', payload: data});
+      userDispatch({ type: 'login', payload: data });
     } else {
       showError(message);
     }
   };
 
   useEffect(() => {
-    getUserData()
+    getUserData();
     if (!initialized.current) {
       initVChartSemiTheme({
         isWatchingThemeSwitch: true,
@@ -468,15 +488,19 @@ const Detail = (props) => {
               >
                 {t('查询')}
               </Button>
-              <Form.Section>
-              </Form.Section>
+              <Form.Section></Form.Section>
             </>
           </Form>
           <Spin spinning={loading}>
-            <Row gutter={{ xs: 16, sm: 16, md: 16, lg: 24, xl: 24, xxl: 24 }} style={{marginTop: 20}} type="flex" justify="space-between">
-              <Col span={styleState.isMobile?24:8}>
+            <Row
+              gutter={{ xs: 16, sm: 16, md: 16, lg: 24, xl: 24, xxl: 24 }}
+              style={{ marginTop: 20 }}
+              type='flex'
+              justify='space-between'
+            >
+              <Col span={styleState.isMobile ? 24 : 8}>
                 <Card className='panel-desc-card'>
-                  <Descriptions row size="small">
+                  <Descriptions row size='small'>
                     <Descriptions.Item itemKey={t('当前余额')}>
                       {renderQuota(userState?.user?.quota)}
                     </Descriptions.Item>
@@ -489,9 +513,9 @@ const Detail = (props) => {
                   </Descriptions>
                 </Card>
               </Col>
-              <Col span={styleState.isMobile?24:8}>
+              <Col span={styleState.isMobile ? 24 : 8}>
                 <Card>
-                  <Descriptions row size="small">
+                  <Descriptions row size='small'>
                     <Descriptions.Item itemKey={t('统计额度')}>
                       {renderQuota(consumeQuota)}
                     </Descriptions.Item>
@@ -508,40 +532,43 @@ const Detail = (props) => {
                 <Card>
                   <Descriptions row size='small'>
                     <Descriptions.Item itemKey={t('平均RPM')}>
-                      {(times /
+                      {(
+                        times /
                         ((Date.parse(end_timestamp) -
                           Date.parse(start_timestamp)) /
-                          60000)).toFixed(3)}
+                          60000)
+                      ).toFixed(3)}
                     </Descriptions.Item>
                     <Descriptions.Item itemKey={t('平均TPM')}>
-                      {(consumeTokens /
+                      {(
+                        consumeTokens /
                         ((Date.parse(end_timestamp) -
                           Date.parse(start_timestamp)) /
-                          60000)).toFixed(3)}
+                          60000)
+                      ).toFixed(3)}
                     </Descriptions.Item>
                   </Descriptions>
                 </Card>
               </Col>
             </Row>
-            <Card style={{marginTop: 20}}>
-              <Tabs type="line" defaultActiveKey="1">
-                <Tabs.TabPane tab={t('消耗分布')} itemKey="1">
+            <Card style={{ marginTop: 20 }}>
+              <Tabs type='line' defaultActiveKey='1'>
+                <Tabs.TabPane tab={t('消耗分布')} itemKey='1'>
                   <div style={{ height: 500 }}>
                     <VChart
                       spec={spec_line}
-                      option={{ mode: "desktop-browser" }}
+                      option={{ mode: 'desktop-browser' }}
                     />
                   </div>
                 </Tabs.TabPane>
-                <Tabs.TabPane tab={t('调用次数分布')} itemKey="2">
+                <Tabs.TabPane tab={t('调用次数分布')} itemKey='2'>
                   <div style={{ height: 500 }}>
                     <VChart
                       spec={spec_pie}
-                      option={{ mode: "desktop-browser" }}
+                      option={{ mode: 'desktop-browser' }}
                     />
                   </div>
                 </Tabs.TabPane>
-
               </Tabs>
             </Card>
           </Spin>

+ 20 - 16
web/src/pages/Home/index.js

@@ -40,19 +40,19 @@ const Home = () => {
       setHomePageContent(content);
       localStorage.setItem('home_page_content', content);
 
-        // 如果内容是 URL,则发送主题模式
-        if (data.startsWith('https://')) {
-            const iframe = document.querySelector('iframe');
-            if (iframe) {
-                const theme = localStorage.getItem('theme-mode') || 'light';
-                // 测试是否正确传递theme-mode给iframe
-                // console.log('Sending theme-mode to iframe:', theme); 
-                iframe.onload = () => {
-                    iframe.contentWindow.postMessage({ themeMode: theme }, '*');
-                    iframe.contentWindow.postMessage({ lang: i18n.language }, '*');
-                };
-            }
+      // 如果内容是 URL,则发送主题模式
+      if (data.startsWith('https://')) {
+        const iframe = document.querySelector('iframe');
+        if (iframe) {
+          const theme = localStorage.getItem('theme-mode') || 'light';
+          // 测试是否正确传递theme-mode给iframe
+          // console.log('Sending theme-mode to iframe:', theme);
+          iframe.onload = () => {
+            iframe.contentWindow.postMessage({ themeMode: theme }, '*');
+            iframe.contentWindow.postMessage({ lang: i18n.language }, '*');
+          };
         }
+      }
     } else {
       showError(message);
       setHomePageContent('加载首页内容失败...');
@@ -95,7 +95,9 @@ const Home = () => {
                     </span>
                   }
                 >
-                  <p>{t('名称')}:{statusState?.status?.system_name}</p>
+                  <p>
+                    {t('名称')}:{statusState?.status?.system_name}
+                  </p>
                   <p>
                     {t('版本')}:
                     {statusState?.status?.version
@@ -123,7 +125,9 @@ const Home = () => {
                       Apache-2.0 License
                     </a>
                   </p>
-                  <p>{t('启动时间')}:{getStartTimeString()}</p>
+                  <p>
+                    {t('启动时间')}:{getStartTimeString()}
+                  </p>
                 </Card>
               </Col>
               <Col span={12}>
@@ -155,8 +159,8 @@ const Home = () => {
                   <p>
                     {t('OIDC 身份验证')}:
                     {statusState?.status?.oidc === true
-                        ? t('已启用')
-                        : t('未启用')}
+                      ? t('已启用')
+                      : t('未启用')}
                   </p>
                   <p>
                     {t('微信身份验证')}:

+ 149 - 106
web/src/pages/Playground/Playground.js

@@ -1,8 +1,23 @@
 import React, { useCallback, useContext, useEffect, useState } from 'react';
 import { useNavigate, useSearchParams } from 'react-router-dom';
 import { UserContext } from '../../context/User/index.js';
-import { API, getUserIdFromLocalStorage, showError } from '../../helpers/index.js';
-import { Card, Chat, Input, Layout, Select, Slider, TextArea, Typography, Button, Highlight } from '@douyinfe/semi-ui';
+import {
+  API,
+  getUserIdFromLocalStorage,
+  showError,
+} from '../../helpers/index.js';
+import {
+  Card,
+  Chat,
+  Input,
+  Layout,
+  Select,
+  Slider,
+  TextArea,
+  Typography,
+  Button,
+  Highlight,
+} from '@douyinfe/semi-ui';
 import { SSE } from 'sse';
 import { IconSetting } from '@douyinfe/semi-icons';
 import { StyleContext } from '../../context/Style/index.js';
@@ -12,26 +27,28 @@ import { renderGroupOption, truncateText } from '../../helpers/render.js';
 const roleInfo = {
   user: {
     name: 'User',
-    avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png'
+    avatar:
+      'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png',
   },
   assistant: {
     name: 'Assistant',
-    avatar: 'logo.png'
+    avatar: 'logo.png',
   },
   system: {
     name: 'System',
-    avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png'
-  }
-}
+    avatar:
+      'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png',
+  },
+};
 
 let id = 4;
 function getId() {
-  return `${id++}`
+  return `${id++}`;
 }
 
 const Playground = () => {
   const { t } = useTranslation();
-  
+
   const defaultMessage = [
     {
       role: 'user',
@@ -44,7 +61,7 @@ const Playground = () => {
       id: '3',
       createAt: 1715676751919,
       content: t('你好,请问有什么可以帮助您的吗?'),
-    }
+    },
   ];
 
   const [inputs, setInputs] = useState({
@@ -56,7 +73,9 @@ const Playground = () => {
   const [searchParams, setSearchParams] = useSearchParams();
   const [userState, userDispatch] = useContext(UserContext);
   const [status, setStatus] = useState({});
-  const [systemPrompt, setSystemPrompt] = useState('You are a helpful assistant. You can help me by answering my questions. You can also ask me questions.');
+  const [systemPrompt, setSystemPrompt] = useState(
+    'You are a helpful assistant. You can help me by answering my questions. You can also ask me questions.',
+  );
   const [message, setMessage] = useState(defaultMessage);
   const [models, setModels] = useState([]);
   const [groups, setGroups] = useState([]);
@@ -99,26 +118,35 @@ const Playground = () => {
     const { success, message, data } = res.data;
     if (success) {
       let localGroupOptions = Object.entries(data).map(([group, info]) => ({
-        label: truncateText(info.desc, "50%"),
+        label: truncateText(info.desc, '50%'),
         value: group,
         ratio: info.ratio,
-        fullLabel: info.desc // 保存完整文本用于tooltip
+        fullLabel: info.desc, // 保存完整文本用于tooltip
       }));
 
       if (localGroupOptions.length === 0) {
-        localGroupOptions = [{
-          label: t('用户分组'),
-          value: '',
-          ratio: 1
-        }];
+        localGroupOptions = [
+          {
+            label: t('用户分组'),
+            value: '',
+            ratio: 1,
+          },
+        ];
       } else {
         const localUser = JSON.parse(localStorage.getItem('user'));
-        const userGroup = (userState.user && userState.user.group) || (localUser && localUser.group);
-        
+        const userGroup =
+          (userState.user && userState.user.group) ||
+          (localUser && localUser.group);
+
         if (userGroup) {
-          const userGroupIndex = localGroupOptions.findIndex(g => g.value === userGroup);
+          const userGroupIndex = localGroupOptions.findIndex(
+            (g) => g.value === userGroup,
+          );
           if (userGroupIndex > -1) {
-            const userGroupOption = localGroupOptions.splice(userGroupIndex, 1)[0];
+            const userGroupOption = localGroupOptions.splice(
+              userGroupIndex,
+              1,
+            )[0];
             localGroupOptions.unshift(userGroupOption);
           }
         }
@@ -135,7 +163,7 @@ const Playground = () => {
     border: '1px solid var(--semi-color-border)',
     borderRadius: '16px',
     margin: '0px 8px',
-  }
+  };
 
   const getSystemMessage = () => {
     if (systemPrompt !== '') {
@@ -144,22 +172,22 @@ const Playground = () => {
         id: '1',
         createAt: 1715676751919,
         content: systemPrompt,
-      }
+      };
     }
-  }
+  };
 
   let handleSSE = (payload) => {
     let source = new SSE('/pg/chat/completions', {
       headers: {
-        "Content-Type": "application/json",
-        "New-Api-User": getUserIdFromLocalStorage(),
+        'Content-Type': 'application/json',
+        'New-Api-User': getUserIdFromLocalStorage(),
       },
-      method: "POST",
+      method: 'POST',
       payload: JSON.stringify(payload),
     });
-    source.addEventListener("message", (e) => {
+    source.addEventListener('message', (e) => {
       // 只有收到 [DONE] 时才结束
-      if (e.data === "[DONE]") {
+      if (e.data === '[DONE]') {
         source.close();
         completeMessage();
         return;
@@ -172,12 +200,12 @@ const Playground = () => {
       }
     });
 
-    source.addEventListener("error", (e) => {
-      generateMockResponse(e.data)
-      completeMessage('error')
+    source.addEventListener('error', (e) => {
+      generateMockResponse(e.data);
+      completeMessage('error');
     });
 
-    source.addEventListener("readystatechange", (e) => {
+    source.addEventListener('readystatechange', (e) => {
       if (e.readyState >= 2) {
         if (source.status === undefined) {
           source.close();
@@ -186,55 +214,58 @@ const Playground = () => {
       }
     });
     source.stream();
-  }
+  };
 
-  const onMessageSend = useCallback((content, attachment) => {
-    console.log("attachment: ", attachment);
-    setMessage((prevMessage) => {
-      const newMessage = [
-        ...prevMessage,
-        {
-          role: 'user',
-          content: content,
-          createAt: Date.now(),
-          id: getId()
-        }
-      ];
+  const onMessageSend = useCallback(
+    (content, attachment) => {
+      console.log('attachment: ', attachment);
+      setMessage((prevMessage) => {
+        const newMessage = [
+          ...prevMessage,
+          {
+            role: 'user',
+            content: content,
+            createAt: Date.now(),
+            id: getId(),
+          },
+        ];
 
-      // 将 getPayload 移到这里
-      const getPayload = () => {
-        let systemMessage = getSystemMessage();
-        let messages = newMessage.map((item) => {
-          return {
-            role: item.role,
-            content: item.content,
+        // 将 getPayload 移到这里
+        const getPayload = () => {
+          let systemMessage = getSystemMessage();
+          let messages = newMessage.map((item) => {
+            return {
+              role: item.role,
+              content: item.content,
+            };
+          });
+          if (systemMessage) {
+            messages.unshift(systemMessage);
           }
-        });
-        if (systemMessage) {
-          messages.unshift(systemMessage);
-        }
-        return {
-          messages: messages,
-          stream: true,
-          model: inputs.model,
-          group: inputs.group,
-          max_tokens: parseInt(inputs.max_tokens),
-          temperature: inputs.temperature,
+          return {
+            messages: messages,
+            stream: true,
+            model: inputs.model,
+            group: inputs.group,
+            max_tokens: parseInt(inputs.max_tokens),
+            temperature: inputs.temperature,
+          };
         };
-      };
 
-      // 使用更新后的消息状态调用 handleSSE
-      handleSSE(getPayload());
-      newMessage.push({
-        role: 'assistant',
-        content: '',
-        createAt: Date.now(),
-        id: getId(),
-        status: 'loading'
+        // 使用更新后的消息状态调用 handleSSE
+        handleSSE(getPayload());
+        newMessage.push({
+          role: 'assistant',
+          content: '',
+          createAt: Date.now(),
+          id: getId(),
+          status: 'loading',
+        });
+        return newMessage;
       });
-      return newMessage;
-    });
-  }, [getSystemMessage]);
+    },
+    [getSystemMessage],
+  );
 
   const completeMessage = useCallback((status = 'complete') => {
     // console.log("Complete Message: ", status)
@@ -244,27 +275,27 @@ const Playground = () => {
       if (lastMessage.status === 'complete' || lastMessage.status === 'error') {
         return prevMessage;
       }
-      return [
-        ...prevMessage.slice(0, -1),
-        { ...lastMessage, status: status }
-      ];
+      return [...prevMessage.slice(0, -1), { ...lastMessage, status: status }];
     });
-  }, [])
+  }, []);
 
   const generateMockResponse = useCallback((content) => {
     // console.log("Generate Mock Response: ", content);
     setMessage((message) => {
       const lastMessage = message[message.length - 1];
-      let newMessage = {...lastMessage};
-      if (lastMessage.status === 'loading' || lastMessage.status === 'incomplete') {
+      let newMessage = { ...lastMessage };
+      if (
+        lastMessage.status === 'loading' ||
+        lastMessage.status === 'incomplete'
+      ) {
         newMessage = {
           ...newMessage,
           content: (lastMessage.content || '') + content,
-          status: 'incomplete'
-        }
+          status: 'incomplete',
+        };
       }
-      return [ ...message.slice(0, -1), newMessage ]
-    })
+      return [...message.slice(0, -1), newMessage];
+    });
   }, []);
 
   const SettingsToggle = () => {
@@ -285,34 +316,47 @@ const Playground = () => {
           boxShadow: '2px 0 8px rgba(0, 0, 0, 0.15)',
         }}
         onClick={() => setShowSettings(!showSettings)}
-        theme="solid"
-        type="primary"
+        theme='solid'
+        type='primary'
       />
     );
   };
 
   function CustomInputRender(props) {
     const { detailProps } = props;
-    const { clearContextNode, uploadNode, inputNode, sendNode, onClick } = detailProps;
+    const { clearContextNode, uploadNode, inputNode, sendNode, onClick } =
+      detailProps;
 
-    return <div style={{margin: '8px 16px', display: 'flex', flexDirection:'row',
-      alignItems: 'flex-end', borderRadius: 16,padding: 10, border: '1px solid var(--semi-color-border)'}}
-                onClick={onClick}
-    >
-      {/*{uploadNode}*/}
-      {inputNode}
-      {sendNode}
-    </div>
+    return (
+      <div
+        style={{
+          margin: '8px 16px',
+          display: 'flex',
+          flexDirection: 'row',
+          alignItems: 'flex-end',
+          borderRadius: 16,
+          padding: 10,
+          border: '1px solid var(--semi-color-border)',
+        }}
+        onClick={onClick}
+      >
+        {/*{uploadNode}*/}
+        {inputNode}
+        {sendNode}
+      </div>
+    );
   }
 
   const renderInputArea = useCallback((props) => {
-    return (<CustomInputRender {...props} />)
+    return <CustomInputRender {...props} />;
   }, []);
 
   return (
-    <Layout style={{height: '100%'}}>
+    <Layout style={{ height: '100%' }}>
       {(showSettings || !styleState.isMobile) && (
-        <Layout.Sider style={{ display: styleState.isMobile ? 'block' : 'initial' }}>
+        <Layout.Sider
+          style={{ display: styleState.isMobile ? 'block' : 'initial' }}
+        >
           <Card style={commonOuterStyle}>
             <div style={{ marginTop: 10 }}>
               <Typography.Text strong>{t('分组')}:</Typography.Text>
@@ -390,18 +434,17 @@ const Playground = () => {
                 setSystemPrompt(value);
               }}
             />
-
           </Card>
         </Layout.Sider>
       )}
       <Layout.Content>
-        <div style={{height: '100%', position: 'relative'}}>
+        <div style={{ height: '100%', position: 'relative' }}>
           <SettingsToggle />
           <Chat
             chatBoxRenderConfig={{
               renderChatBoxAction: () => {
-                return <div></div>
-              }
+                return <div></div>;
+              },
             }}
             renderInputArea={renderInputArea}
             roleConfig={roleInfo}

+ 8 - 2
web/src/pages/Redemption/EditRedemption.js

@@ -8,7 +8,11 @@ import {
   showError,
   showSuccess,
 } from '../../helpers';
-import { getQuotaPerUnit, renderQuota, renderQuotaWithPrompt } from '../../helpers/render';
+import {
+  getQuotaPerUnit,
+  renderQuota,
+  renderQuotaWithPrompt,
+} from '../../helpers/render';
 import {
   AutoComplete,
   Button,
@@ -171,7 +175,9 @@ const EditRedemption = (props) => {
           />
           <Divider />
           <div style={{ marginTop: 20 }}>
-            <Typography.Text>{t('额度') + renderQuotaWithPrompt(quota)}</Typography.Text>
+            <Typography.Text>
+              {t('额度') + renderQuotaWithPrompt(quota)}
+            </Typography.Text>
           </div>
           <AutoComplete
             style={{ marginTop: 8 }}

+ 9 - 9
web/src/pages/Redemption/index.js

@@ -9,14 +9,14 @@ const Redemption = () => {
     <>
       <Layout>
         <Layout.Header>
-        <h3>{t('管理兑换码')}</h3>
-      </Layout.Header>
-      <Layout.Content>
-        <RedemptionsTable />
-      </Layout.Content>
-    </Layout>
-  </>
-);
-}
+          <h3>{t('管理兑换码')}</h3>
+        </Layout.Header>
+        <Layout.Content>
+          <RedemptionsTable />
+        </Layout.Content>
+      </Layout>
+    </>
+  );
+};
 
 export default Redemption;

+ 60 - 22
web/src/pages/Setting/Model/SettingClaudeModel.js

@@ -5,23 +5,27 @@ import {
   API,
   showError,
   showSuccess,
-  showWarning, verifyJSON
+  showWarning,
+  verifyJSON,
 } from '../../../helpers';
 import { useTranslation } from 'react-i18next';
 import Text from '@douyinfe/semi-ui/lib/es/typography/text';
 
 const CLAUDE_HEADER = {
   'claude-3-7-sonnet-20250219-thinking': {
-    'anthropic-beta': ['output-128k-2025-02-19', 'token-efficient-tools-2025-02-19'],
-  }
+    'anthropic-beta': [
+      'output-128k-2025-02-19',
+      'token-efficient-tools-2025-02-19',
+    ],
+  },
 };
 
 const CLAUDE_DEFAULT_MAX_TOKENS = {
-  'default': 8192,
-  "claude-3-haiku-20240307": 4096,
-  "claude-3-opus-20240229": 4096,
+  default: 8192,
+  'claude-3-haiku-20240307': 4096,
+  'claude-3-opus-20240229': 4096,
   'claude-3-7-sonnet-20250219-thinking': 8192,
-}
+};
 
 export default function SettingClaudeModel(props) {
   const { t } = useTranslation();
@@ -41,7 +45,7 @@ export default function SettingClaudeModel(props) {
     if (!updateArray.length) return showWarning(t('你似乎并没有修改什么'));
     const requestQueue = updateArray.map((item) => {
       let value = String(inputs[item.key]);
-      
+
       return API.put('/api/option/', {
         key: item.key,
         value,
@@ -53,7 +57,8 @@ export default function SettingClaudeModel(props) {
         if (requestQueue.length === 1) {
           if (res.includes(undefined)) return;
         } else if (requestQueue.length > 1) {
-          if (res.includes(undefined)) return showError(t('部分保存失败,请重试'));
+          if (res.includes(undefined))
+            return showError(t('部分保存失败,请重试'));
         }
         showSuccess(t('保存成功'));
         props.refresh();
@@ -92,18 +97,29 @@ export default function SettingClaudeModel(props) {
                 <Form.TextArea
                   label={t('Claude请求头覆盖')}
                   field={'claude.model_headers_settings'}
-                  placeholder={t('为一个 JSON 文本,例如:') + '\n' + JSON.stringify(CLAUDE_HEADER, null, 2)}
-                  extraText={t('示例') + '\n' + JSON.stringify(CLAUDE_HEADER, null, 2)}
+                  placeholder={
+                    t('为一个 JSON 文本,例如:') +
+                    '\n' +
+                    JSON.stringify(CLAUDE_HEADER, null, 2)
+                  }
+                  extraText={
+                    t('示例') + '\n' + JSON.stringify(CLAUDE_HEADER, null, 2)
+                  }
                   autosize={{ minRows: 6, maxRows: 12 }}
                   trigger='blur'
                   stopValidateWithError
                   rules={[
                     {
                       validator: (rule, value) => verifyJSON(value),
-                      message: t('不是合法的 JSON 字符串')
-                    }
+                      message: t('不是合法的 JSON 字符串'),
+                    },
                   ]}
-                  onChange={(value) => setInputs({ ...inputs, 'claude.model_headers_settings': value })}
+                  onChange={(value) =>
+                    setInputs({
+                      ...inputs,
+                      'claude.model_headers_settings': value,
+                    })
+                  }
                 />
               </Col>
             </Row>
@@ -112,18 +128,28 @@ export default function SettingClaudeModel(props) {
                 <Form.TextArea
                   label={t('缺省 MaxTokens')}
                   field={'claude.default_max_tokens'}
-                  placeholder={t('为一个 JSON 文本,例如:') + '\n' + JSON.stringify(CLAUDE_DEFAULT_MAX_TOKENS, null, 2)}
-                  extraText={t('示例') + '\n' + JSON.stringify(CLAUDE_DEFAULT_MAX_TOKENS, null, 2)}
+                  placeholder={
+                    t('为一个 JSON 文本,例如:') +
+                    '\n' +
+                    JSON.stringify(CLAUDE_DEFAULT_MAX_TOKENS, null, 2)
+                  }
+                  extraText={
+                    t('示例') +
+                    '\n' +
+                    JSON.stringify(CLAUDE_DEFAULT_MAX_TOKENS, null, 2)
+                  }
                   autosize={{ minRows: 6, maxRows: 12 }}
                   trigger='blur'
                   stopValidateWithError
                   rules={[
                     {
                       validator: (rule, value) => verifyJSON(value),
-                      message: t('不是合法的 JSON 字符串')
-                    }
+                      message: t('不是合法的 JSON 字符串'),
+                    },
                   ]}
-                  onChange={(value) => setInputs({ ...inputs, 'claude.default_max_tokens': value })}
+                  onChange={(value) =>
+                    setInputs({ ...inputs, 'claude.default_max_tokens': value })
+                  }
                 />
               </Col>
             </Row>
@@ -132,7 +158,12 @@ export default function SettingClaudeModel(props) {
                 <Form.Switch
                   label={t('启用Claude思考适配(-thinking后缀)')}
                   field={'claude.thinking_adapter_enabled'}
-                  onChange={(value) => setInputs({ ...inputs, 'claude.thinking_adapter_enabled': value })}
+                  onChange={(value) =>
+                    setInputs({
+                      ...inputs,
+                      'claude.thinking_adapter_enabled': value,
+                    })
+                  }
                 />
               </Col>
             </Row>
@@ -140,7 +171,9 @@ export default function SettingClaudeModel(props) {
               <Col span={16}>
                 {/*//展示MaxTokens和BudgetTokens的计算公式, 并展示实际数字*/}
                 <Text>
-                  {t('Claude思考适配 BudgetTokens = MaxTokens * BudgetTokens 百分比')}
+                  {t(
+                    'Claude思考适配 BudgetTokens = MaxTokens * BudgetTokens 百分比',
+                  )}
                 </Text>
               </Col>
             </Row>
@@ -153,7 +186,12 @@ export default function SettingClaudeModel(props) {
                   extraText={t('0.1-1之间的小数')}
                   min={0.1}
                   max={1}
-                  onChange={(value) => setInputs({ ...inputs, 'claude.thinking_adapter_budget_tokens_percentage': value })}
+                  onChange={(value) =>
+                    setInputs({
+                      ...inputs,
+                      'claude.thinking_adapter_budget_tokens_percentage': value,
+                    })
+                  }
                 />
               </Col>
             </Row>

+ 30 - 15
web/src/pages/Setting/Model/SettingGeminiModel.js

@@ -5,20 +5,20 @@ import {
   API,
   showError,
   showSuccess,
-  showWarning, verifyJSON
+  showWarning,
+  verifyJSON,
 } from '../../../helpers';
 import { useTranslation } from 'react-i18next';
 
 const GEMINI_SETTING_EXAMPLE = {
-  'default': 'OFF',
-  'HARM_CATEGORY_CIVIC_INTEGRITY': 'BLOCK_NONE',
+  default: 'OFF',
+  HARM_CATEGORY_CIVIC_INTEGRITY: 'BLOCK_NONE',
 };
 
 const GEMINI_VERSION_EXAMPLE = {
-  'default': 'v1beta',
+  default: 'v1beta',
 };
 
-
 export default function SettingGeminiModel(props) {
   const { t } = useTranslation();
 
@@ -52,7 +52,8 @@ export default function SettingGeminiModel(props) {
         if (requestQueue.length === 1) {
           if (res.includes(undefined)) return;
         } else if (requestQueue.length > 1) {
-          if (res.includes(undefined)) return showError(t('部分保存失败,请重试'));
+          if (res.includes(undefined))
+            return showError(t('部分保存失败,请重试'));
         }
         showSuccess(t('保存成功'));
         props.refresh();
@@ -90,19 +91,27 @@ export default function SettingGeminiModel(props) {
               <Col xs={24} sm={12} md={8} lg={8} xl={8}>
                 <Form.TextArea
                   label={t('Gemini安全设置')}
-                  placeholder={t('为一个 JSON 文本,例如:') + '\n' + JSON.stringify(GEMINI_SETTING_EXAMPLE, null, 2)}
+                  placeholder={
+                    t('为一个 JSON 文本,例如:') +
+                    '\n' +
+                    JSON.stringify(GEMINI_SETTING_EXAMPLE, null, 2)
+                  }
                   field={'gemini.safety_settings'}
-                  extraText={t('default为默认设置,可单独设置每个分类的安全等级')}
+                  extraText={t(
+                    'default为默认设置,可单独设置每个分类的安全等级',
+                  )}
                   autosize={{ minRows: 6, maxRows: 12 }}
                   trigger='blur'
                   stopValidateWithError
                   rules={[
                     {
                       validator: (rule, value) => verifyJSON(value),
-                      message: t('不是合法的 JSON 字符串')
-                    }
+                      message: t('不是合法的 JSON 字符串'),
+                    },
                   ]}
-                  onChange={(value) => setInputs({ ...inputs, 'gemini.safety_settings': value })}
+                  onChange={(value) =>
+                    setInputs({ ...inputs, 'gemini.safety_settings': value })
+                  }
                 />
               </Col>
             </Row>
@@ -110,7 +119,11 @@ export default function SettingGeminiModel(props) {
               <Col xs={24} sm={12} md={8} lg={8} xl={8}>
                 <Form.TextArea
                   label={t('Gemini版本设置')}
-                  placeholder={t('为一个 JSON 文本,例如:') + '\n' + JSON.stringify(GEMINI_VERSION_EXAMPLE, null, 2)}
+                  placeholder={
+                    t('为一个 JSON 文本,例如:') +
+                    '\n' +
+                    JSON.stringify(GEMINI_VERSION_EXAMPLE, null, 2)
+                  }
                   field={'gemini.version_settings'}
                   extraText={t('default为默认设置,可单独设置每个模型的版本')}
                   autosize={{ minRows: 6, maxRows: 12 }}
@@ -119,10 +132,12 @@ export default function SettingGeminiModel(props) {
                   rules={[
                     {
                       validator: (rule, value) => verifyJSON(value),
-                      message: t('不是合法的 JSON 字符串')
-                    }
+                      message: t('不是合法的 JSON 字符串'),
+                    },
                   ]}
-                  onChange={(value) => setInputs({ ...inputs, 'gemini.version_settings': value })}
+                  onChange={(value) =>
+                    setInputs({ ...inputs, 'gemini.version_settings': value })
+                  }
                 />
               </Col>
             </Row>

+ 13 - 4
web/src/pages/Setting/Model/SettingGlobalModel.js

@@ -5,7 +5,8 @@ import {
   API,
   showError,
   showSuccess,
-  showWarning, verifyJSON
+  showWarning,
+  verifyJSON,
 } from '../../../helpers';
 import { useTranslation } from 'react-i18next';
 
@@ -38,7 +39,8 @@ export default function SettingGlobalModel(props) {
         if (requestQueue.length === 1) {
           if (res.includes(undefined)) return;
         } else if (requestQueue.length > 1) {
-          if (res.includes(undefined)) return showError(t('部分保存失败,请重试'));
+          if (res.includes(undefined))
+            return showError(t('部分保存失败,请重试'));
         }
         showSuccess(t('保存成功'));
         props.refresh();
@@ -77,8 +79,15 @@ export default function SettingGlobalModel(props) {
                 <Form.Switch
                   label={t('启用请求透传')}
                   field={'global.pass_through_request_enabled'}
-                  onChange={(value) => setInputs({ ...inputs, 'global.pass_through_request_enabled': value })}
-                  extraText={'开启后,所有请求将直接透传给上游,不会进行任何处理(重定向和渠道适配也将失效),请谨慎开启'}
+                  onChange={(value) =>
+                    setInputs({
+                      ...inputs,
+                      'global.pass_through_request_enabled': value,
+                    })
+                  }
+                  extraText={
+                    '开启后,所有请求将直接透传给上游,不会进行任何处理(重定向和渠道适配也将失效),请谨慎开启'
+                  }
                 />
               </Col>
             </Row>

+ 56 - 43
web/src/pages/Setting/Operation/GroupRatioSettings.js

@@ -15,50 +15,59 @@ export default function GroupRatioSettings(props) {
   const [loading, setLoading] = useState(false);
   const [inputs, setInputs] = useState({
     GroupRatio: '',
-    UserUsableGroups: ''
+    UserUsableGroups: '',
   });
   const refForm = useRef();
   const [inputsRow, setInputsRow] = useState(inputs);
 
   async function onSubmit() {
     try {
-      await refForm.current.validate().then(() => {
-        const updateArray = compareObjects(inputs, inputsRow);
-        if (!updateArray.length) return showWarning(t('你似乎并没有修改什么'));
-        
-        const requestQueue = updateArray.map((item) => {
-          const value = typeof inputs[item.key] === 'boolean' 
-            ? String(inputs[item.key]) 
-            : inputs[item.key];
-          return API.put('/api/option/', { key: item.key, value });
-        });
+      await refForm.current
+        .validate()
+        .then(() => {
+          const updateArray = compareObjects(inputs, inputsRow);
+          if (!updateArray.length)
+            return showWarning(t('你似乎并没有修改什么'));
 
-        setLoading(true);
-        Promise.all(requestQueue)
-          .then((res) => {
-            if (res.includes(undefined)) {
-              return showError(requestQueue.length > 1 ? t('部分保存失败,请重试') : t('保存失败'));
-            }
-            
-            for (let i = 0; i < res.length; i++) {
-              if (!res[i].data.success) {
-                return showError(res[i].data.message);
-              }
-            }
-            
-            showSuccess(t('保存成功'));
-            props.refresh();
-          })
-          .catch(error => {
-            console.error('Unexpected error:', error);
-            showError(t('保存失败,请重试'));
-          })
-          .finally(() => {
-            setLoading(false);
+          const requestQueue = updateArray.map((item) => {
+            const value =
+              typeof inputs[item.key] === 'boolean'
+                ? String(inputs[item.key])
+                : inputs[item.key];
+            return API.put('/api/option/', { key: item.key, value });
           });
-      }).catch(() => {
-        showError(t('请检查输入'));
-      });
+
+          setLoading(true);
+          Promise.all(requestQueue)
+            .then((res) => {
+              if (res.includes(undefined)) {
+                return showError(
+                  requestQueue.length > 1
+                    ? t('部分保存失败,请重试')
+                    : t('保存失败'),
+                );
+              }
+
+              for (let i = 0; i < res.length; i++) {
+                if (!res[i].data.success) {
+                  return showError(res[i].data.message);
+                }
+              }
+
+              showSuccess(t('保存成功'));
+              props.refresh();
+            })
+            .catch((error) => {
+              console.error('Unexpected error:', error);
+              showError(t('保存失败,请重试'));
+            })
+            .finally(() => {
+              setLoading(false);
+            });
+        })
+        .catch(() => {
+          showError(t('请检查输入'));
+        });
     } catch (error) {
       showError(t('请检查输入'));
       console.error(error);
@@ -97,10 +106,12 @@ export default function GroupRatioSettings(props) {
                 rules={[
                   {
                     validator: (rule, value) => verifyJSON(value),
-                    message: t('不是合法的 JSON 字符串')
-                  }
+                    message: t('不是合法的 JSON 字符串'),
+                  },
                 ]}
-                onChange={(value) => setInputs({ ...inputs, GroupRatio: value })}
+                onChange={(value) =>
+                  setInputs({ ...inputs, GroupRatio: value })
+                }
               />
             </Col>
           </Row>
@@ -116,10 +127,12 @@ export default function GroupRatioSettings(props) {
                 rules={[
                   {
                     validator: (rule, value) => verifyJSON(value),
-                    message: t('不是合法的 JSON 字符串')
-                  }
+                    message: t('不是合法的 JSON 字符串'),
+                  },
                 ]}
-                onChange={(value) => setInputs({ ...inputs, UserUsableGroups: value })}
+                onChange={(value) =>
+                  setInputs({ ...inputs, UserUsableGroups: value })
+                }
               />
             </Col>
           </Row>
@@ -128,4 +141,4 @@ export default function GroupRatioSettings(props) {
       <Button onClick={onSubmit}>{t('保存分组倍率设置')}</Button>
     </Spin>
   );
-} 
+}

+ 77 - 50
web/src/pages/Setting/Operation/ModelRatioSettings.js

@@ -1,5 +1,13 @@
 import React, { useEffect, useState, useRef } from 'react';
-import { Button, Col, Form, Popconfirm, Row, Space, Spin } from '@douyinfe/semi-ui';
+import {
+  Button,
+  Col,
+  Form,
+  Popconfirm,
+  Row,
+  Space,
+  Spin,
+} from '@douyinfe/semi-ui';
 import {
   compareObjects,
   API,
@@ -24,43 +32,52 @@ export default function ModelRatioSettings(props) {
 
   async function onSubmit() {
     try {
-      await refForm.current.validate().then(() => {
-        const updateArray = compareObjects(inputs, inputsRow);
-        if (!updateArray.length) return showWarning(t('你似乎并没有修改什么'));
-        
-        const requestQueue = updateArray.map((item) => {
-          const value = typeof inputs[item.key] === 'boolean' 
-            ? String(inputs[item.key]) 
-            : inputs[item.key];
-          return API.put('/api/option/', { key: item.key, value });
-        });
+      await refForm.current
+        .validate()
+        .then(() => {
+          const updateArray = compareObjects(inputs, inputsRow);
+          if (!updateArray.length)
+            return showWarning(t('你似乎并没有修改什么'));
 
-        setLoading(true);
-        Promise.all(requestQueue)
-          .then((res) => {
-            if (res.includes(undefined)) {
-              return showError(requestQueue.length > 1 ? t('部分保存失败,请重试') : t('保存失败'));
-            }
-            
-            for (let i = 0; i < res.length; i++) {
-              if (!res[i].data.success) {
-                return showError(res[i].data.message);
-              }
-            }
-            
-            showSuccess(t('保存成功'));
-            props.refresh();
-          })
-          .catch(error => {
-            console.error('Unexpected error:', error);
-            showError(t('保存失败,请重试'));
-          })
-          .finally(() => {
-            setLoading(false);
+          const requestQueue = updateArray.map((item) => {
+            const value =
+              typeof inputs[item.key] === 'boolean'
+                ? String(inputs[item.key])
+                : inputs[item.key];
+            return API.put('/api/option/', { key: item.key, value });
           });
-      }).catch(() => {
-        showError(t('请检查输入'));
-      });
+
+          setLoading(true);
+          Promise.all(requestQueue)
+            .then((res) => {
+              if (res.includes(undefined)) {
+                return showError(
+                  requestQueue.length > 1
+                    ? t('部分保存失败,请重试')
+                    : t('保存失败'),
+                );
+              }
+
+              for (let i = 0; i < res.length; i++) {
+                if (!res[i].data.success) {
+                  return showError(res[i].data.message);
+                }
+              }
+
+              showSuccess(t('保存成功'));
+              props.refresh();
+            })
+            .catch((error) => {
+              console.error('Unexpected error:', error);
+              showError(t('保存失败,请重试'));
+            })
+            .finally(() => {
+              setLoading(false);
+            });
+        })
+        .catch(() => {
+          showError(t('请检查输入'));
+        });
     } catch (error) {
       showError(t('请检查输入'));
       console.error(error);
@@ -106,7 +123,9 @@ export default function ModelRatioSettings(props) {
               <Form.TextArea
                 label={t('模型固定价格')}
                 extraText={t('一次调用消耗多少刀,优先级大于模型倍率')}
-                placeholder={t('为一个 JSON 文本,键为模型名称,值为一次调用消耗多少刀,比如 "gpt-4-gizmo-*": 0.1,一次消耗0.1刀')}
+                placeholder={t(
+                  '为一个 JSON 文本,键为模型名称,值为一次调用消耗多少刀,比如 "gpt-4-gizmo-*": 0.1,一次消耗0.1刀',
+                )}
                 field={'ModelPrice'}
                 autosize={{ minRows: 6, maxRows: 12 }}
                 trigger='blur'
@@ -114,10 +133,12 @@ export default function ModelRatioSettings(props) {
                 rules={[
                   {
                     validator: (rule, value) => verifyJSON(value),
-                    message: '不是合法的 JSON 字符串'
-                  }
+                    message: '不是合法的 JSON 字符串',
+                  },
                 ]}
-                onChange={(value) => setInputs({ ...inputs, ModelPrice: value })}
+                onChange={(value) =>
+                  setInputs({ ...inputs, ModelPrice: value })
+                }
               />
             </Col>
           </Row>
@@ -133,10 +154,12 @@ export default function ModelRatioSettings(props) {
                 rules={[
                   {
                     validator: (rule, value) => verifyJSON(value),
-                    message: '不是合法的 JSON 字符串'
-                  }
+                    message: '不是合法的 JSON 字符串',
+                  },
                 ]}
-                onChange={(value) => setInputs({ ...inputs, ModelRatio: value })}
+                onChange={(value) =>
+                  setInputs({ ...inputs, ModelRatio: value })
+                }
               />
             </Col>
           </Row>
@@ -152,10 +175,12 @@ export default function ModelRatioSettings(props) {
                 rules={[
                   {
                     validator: (rule, value) => verifyJSON(value),
-                    message: '不是合法的 JSON 字符串'
-                  }
+                    message: '不是合法的 JSON 字符串',
+                  },
                 ]}
-                onChange={(value) => setInputs({ ...inputs, CacheRatio: value })}
+                onChange={(value) =>
+                  setInputs({ ...inputs, CacheRatio: value })
+                }
               />
             </Col>
           </Row>
@@ -172,10 +197,12 @@ export default function ModelRatioSettings(props) {
                 rules={[
                   {
                     validator: (rule, value) => verifyJSON(value),
-                    message: '不是合法的 JSON 字符串'
-                  }
+                    message: '不是合法的 JSON 字符串',
+                  },
                 ]}
-                onChange={(value) => setInputs({ ...inputs, CompletionRatio: value })}
+                onChange={(value) =>
+                  setInputs({ ...inputs, CompletionRatio: value })
+                }
               />
             </Col>
           </Row>
@@ -195,4 +222,4 @@ export default function ModelRatioSettings(props) {
       </Space>
     </Spin>
   );
-} 
+}

+ 152 - 93
web/src/pages/Setting/Operation/ModelRationNotSetEditor.js

@@ -1,6 +1,22 @@
 import React, { useEffect, useState } from 'react';
-import { Table, Button, Input, Modal, Form, Space, Typography, Radio, Notification } from '@douyinfe/semi-ui';
-import { IconDelete, IconPlus, IconSearch, IconSave, IconBolt } from '@douyinfe/semi-icons';
+import {
+  Table,
+  Button,
+  Input,
+  Modal,
+  Form,
+  Space,
+  Typography,
+  Radio,
+  Notification,
+} from '@douyinfe/semi-ui';
+import {
+  IconDelete,
+  IconPlus,
+  IconSearch,
+  IconSave,
+  IconBolt,
+} from '@douyinfe/semi-icons';
 import { showError, showSuccess } from '../../../helpers';
 import { API } from '../../../helpers';
 import { useTranslation } from 'react-i18next';
@@ -20,7 +36,8 @@ export default function ModelRatioNotSetEditor(props) {
   const [batchFillType, setBatchFillType] = useState('ratio');
   const [batchFillValue, setBatchFillValue] = useState('');
   const [batchRatioValue, setBatchRatioValue] = useState('');
-  const [batchCompletionRatioValue, setBatchCompletionRatioValue] = useState('');
+  const [batchCompletionRatioValue, setBatchCompletionRatioValue] =
+    useState('');
   const { Text } = Typography;
   // 定义可选的每页显示条数
   const pageSizeOptions = [10, 20, 50, 100];
@@ -38,7 +55,7 @@ export default function ModelRatioNotSetEditor(props) {
       console.error(t('获取启用模型失败:'), error);
       showError(t('获取启用模型失败'));
     }
-  }
+  };
 
   useEffect(() => {
     // 获取所有启用的模型
@@ -52,20 +69,20 @@ export default function ModelRatioNotSetEditor(props) {
       const completionRatio = JSON.parse(props.options.CompletionRatio || '{}');
 
       // 找出所有未设置价格和倍率的模型
-      const unsetModels = enabledModels.filter(modelName => {
+      const unsetModels = enabledModels.filter((modelName) => {
         const hasPrice = modelPrice[modelName] !== undefined;
         const hasRatio = modelRatio[modelName] !== undefined;
-        
+
         // 如果模型没有价格或者没有倍率设置,则显示
         return !hasPrice && !hasRatio;
       });
 
       // 创建模型数据
-      const modelData = unsetModels.map(name => ({
+      const modelData = unsetModels.map((name) => ({
         name,
         price: modelPrice[name] || '',
         ratio: modelRatio[name] || '',
-        completionRatio: completionRatio[name] || ''
+        completionRatio: completionRatio[name] || '',
       }));
 
       setModels(modelData);
@@ -94,8 +111,10 @@ export default function ModelRatioNotSetEditor(props) {
   };
 
   // 在 return 语句之前,先处理过滤和分页逻辑
-  const filteredModels = models.filter(model =>
-    searchText ? model.name.toLowerCase().includes(searchText.toLowerCase()) : true
+  const filteredModels = models.filter((model) =>
+    searchText
+      ? model.name.toLowerCase().includes(searchText.toLowerCase())
+      : true,
   );
 
   // 然后基于过滤后的数据计算分页数据
@@ -106,19 +125,23 @@ export default function ModelRatioNotSetEditor(props) {
     const output = {
       ModelPrice: JSON.parse(props.options.ModelPrice || '{}'),
       ModelRatio: JSON.parse(props.options.ModelRatio || '{}'),
-      CompletionRatio: JSON.parse(props.options.CompletionRatio || '{}')
+      CompletionRatio: JSON.parse(props.options.CompletionRatio || '{}'),
     };
 
     try {
       // 数据转换 - 只处理已修改的模型
-      models.forEach(model => {
+      models.forEach((model) => {
         // 只有当用户设置了值时才更新
         if (model.price !== '') {
           // 如果价格不为空,则转换为浮点数,忽略倍率参数
           output.ModelPrice[model.name] = parseFloat(model.price);
         } else {
-          if (model.ratio !== '') output.ModelRatio[model.name] = parseFloat(model.ratio);
-          if (model.completionRatio !== '') output.CompletionRatio[model.name] = parseFloat(model.completionRatio);
+          if (model.ratio !== '')
+            output.ModelRatio[model.name] = parseFloat(model.ratio);
+          if (model.completionRatio !== '')
+            output.CompletionRatio[model.name] = parseFloat(
+              model.completionRatio,
+            );
         }
       });
 
@@ -126,13 +149,13 @@ export default function ModelRatioNotSetEditor(props) {
       const finalOutput = {
         ModelPrice: JSON.stringify(output.ModelPrice, null, 2),
         ModelRatio: JSON.stringify(output.ModelRatio, null, 2),
-        CompletionRatio: JSON.stringify(output.CompletionRatio, null, 2)
+        CompletionRatio: JSON.stringify(output.CompletionRatio, null, 2),
       };
 
       const requestQueue = Object.entries(finalOutput).map(([key, value]) => {
         return API.put('/api/option/', {
           key,
-          value
+          value,
         });
       });
 
@@ -159,7 +182,6 @@ export default function ModelRatioNotSetEditor(props) {
       props.refresh();
       // 重新获取未设置的模型
       getAllEnabledModels();
-
     } catch (error) {
       console.error(t('保存失败:'), error);
       showError(t('保存失败,请重试'));
@@ -182,9 +204,9 @@ export default function ModelRatioNotSetEditor(props) {
         <Input
           value={text}
           placeholder={t('按量计费')}
-          onChange={value => updateModel(record.name, 'price', value)}
+          onChange={(value) => updateModel(record.name, 'price', value)}
         />
-      )
+      ),
     },
     {
       title: t('模型倍率'),
@@ -195,9 +217,9 @@ export default function ModelRatioNotSetEditor(props) {
           value={text}
           placeholder={record.price !== '' ? t('模型倍率') : t('输入模型倍率')}
           disabled={record.price !== ''}
-          onChange={value => updateModel(record.name, 'ratio', value)}
+          onChange={(value) => updateModel(record.name, 'ratio', value)}
         />
-      )
+      ),
     },
     {
       title: t('补全倍率'),
@@ -208,10 +230,12 @@ export default function ModelRatioNotSetEditor(props) {
           value={text}
           placeholder={record.price !== '' ? t('补全倍率') : t('输入补全倍率')}
           disabled={record.price !== ''}
-          onChange={value => updateModel(record.name, 'completionRatio', value)}
+          onChange={(value) =>
+            updateModel(record.name, 'completionRatio', value)
+          }
         />
-      )
-    }
+      ),
+    },
   ];
 
   const updateModel = (name, field, value) => {
@@ -219,27 +243,28 @@ export default function ModelRatioNotSetEditor(props) {
       showError(t('请输入数字'));
       return;
     }
-    setModels(prev =>
-      prev.map(model =>
-        model.name === name
-          ? { ...model, [field]: value }
-          : model
-      )
+    setModels((prev) =>
+      prev.map((model) =>
+        model.name === name ? { ...model, [field]: value } : model,
+      ),
     );
   };
 
   const addModel = (values) => {
     // 检查模型名称是否存在, 如果存在则拒绝添加
-    if (models.some(model => model.name === values.name)) {
+    if (models.some((model) => model.name === values.name)) {
       showError(t('模型名称已存在'));
       return;
     }
-    setModels(prev => [{
-      name: values.name,
-      price: values.price || '',
-      ratio: values.ratio || '',
-      completionRatio: values.completionRatio || ''
-    }, ...prev]);
+    setModels((prev) => [
+      {
+        name: values.name,
+        price: values.price || '',
+        ratio: values.ratio || '',
+        completionRatio: values.completionRatio || '',
+      },
+      ...prev,
+    ]);
     setVisible(false);
     showSuccess(t('添加成功'));
   };
@@ -272,39 +297,39 @@ export default function ModelRatioNotSetEditor(props) {
     }
 
     // 根据选择的类型批量更新模型
-    setModels(prev => 
-      prev.map(model => {
+    setModels((prev) =>
+      prev.map((model) => {
         if (selectedRowKeys.includes(model.name)) {
           if (batchFillType === 'price') {
             return {
               ...model,
               price: batchFillValue,
               ratio: '',
-              completionRatio: ''
+              completionRatio: '',
             };
           } else if (batchFillType === 'ratio') {
             return {
               ...model,
               price: '',
-              ratio: batchFillValue
+              ratio: batchFillValue,
             };
           } else if (batchFillType === 'completionRatio') {
             return {
               ...model,
               price: '',
-              completionRatio: batchFillValue
+              completionRatio: batchFillValue,
             };
           } else if (batchFillType === 'bothRatio') {
             return {
               ...model,
               price: '',
               ratio: batchRatioValue,
-              completionRatio: batchCompletionRatioValue
+              completionRatio: batchCompletionRatioValue,
             };
           }
         }
         return model;
-      })
+      }),
     );
 
     setBatchVisible(false);
@@ -312,9 +337,14 @@ export default function ModelRatioNotSetEditor(props) {
       title: t('批量设置成功'),
       content: t('已为 {{count}} 个模型设置{{type}}', {
         count: selectedRowKeys.length,
-        type: batchFillType === 'price' ? t('固定价格') : 
-              batchFillType === 'ratio' ? t('模型倍率') : 
-              batchFillType === 'completionRatio' ? t('补全倍率') : t('模型倍率和补全倍率')
+        type:
+          batchFillType === 'price'
+            ? t('固定价格')
+            : batchFillType === 'ratio'
+              ? t('模型倍率')
+              : batchFillType === 'completionRatio'
+                ? t('补全倍率')
+                : t('模型倍率和补全倍率'),
       }),
       duration: 3,
     });
@@ -323,7 +353,7 @@ export default function ModelRatioNotSetEditor(props) {
   const handleBatchTypeChange = (value) => {
     console.log(t('Changing batch type to:'), value);
     setBatchFillType(value);
-    
+
     // 切换类型时清空对应的值
     if (value !== 'bothRatio') {
       setBatchFillValue('');
@@ -342,56 +372,63 @@ export default function ModelRatioNotSetEditor(props) {
 
   return (
     <>
-      <Space vertical align="start" style={{ width: '100%' }}>
+      <Space vertical align='start' style={{ width: '100%' }}>
         <Space>
           <Button icon={<IconPlus />} onClick={() => setVisible(true)}>
             {t('添加模型')}
           </Button>
-          <Button 
-            icon={<IconBolt />} 
-            type="secondary"
+          <Button
+            icon={<IconBolt />}
+            type='secondary'
             onClick={() => setBatchVisible(true)}
             disabled={selectedRowKeys.length === 0}
           >
             {t('批量设置')} ({selectedRowKeys.length})
           </Button>
-          <Button type="primary" icon={<IconSave />} onClick={SubmitData} loading={loading}>
+          <Button
+            type='primary'
+            icon={<IconSave />}
+            onClick={SubmitData}
+            loading={loading}
+          >
             {t('应用更改')}
           </Button>
           <Input
             prefix={<IconSearch />}
             placeholder={t('搜索模型名称')}
             value={searchText}
-            onChange={value => {
-              setSearchText(value)
+            onChange={(value) => {
+              setSearchText(value);
               setCurrentPage(1);
             }}
             style={{ width: 200 }}
           />
         </Space>
 
-        <Text>{t('此页面仅显示未设置价格或倍率的模型,设置后将自动从列表中移除')}</Text>
-        
+        <Text>
+          {t('此页面仅显示未设置价格或倍率的模型,设置后将自动从列表中移除')}
+        </Text>
+
         <Table
           columns={columns}
           dataSource={pagedData}
           rowSelection={rowSelection}
-          rowKey="name"
+          rowKey='name'
           pagination={{
             currentPage: currentPage,
             pageSize: pageSize,
             total: filteredModels.length,
-            onPageChange: page => setCurrentPage(page),
+            onPageChange: (page) => setCurrentPage(page),
             onPageSizeChange: handlePageSizeChange,
             pageSizeOptions: pageSizeOptions,
             formatPageText: (page) =>
               t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
                 start: page.currentStart,
                 end: page.currentEnd,
-                total: filteredModels.length
+                total: filteredModels.length,
               }),
             showTotal: true,
-            showSizeChanger: true
+            showSizeChanger: true,
           }}
           empty={
             <div style={{ textAlign: 'center', padding: '20px' }}>
@@ -412,45 +449,61 @@ export default function ModelRatioNotSetEditor(props) {
       >
         <Form>
           <Form.Input
-            field="name"
+            field='name'
             label={t('模型名称')}
-            placeholder="strawberry"
+            placeholder='strawberry'
             required
-            onChange={value => setCurrentModel(prev => ({ ...prev, name: value }))}
+            onChange={(value) =>
+              setCurrentModel((prev) => ({ ...prev, name: value }))
+            }
           />
           <Form.Switch
-            field="priceMode"
-            label={<>{t('定价模式')}:{currentModel?.priceMode ? t("固定价格") : t("倍率模式")}</>}
-            onChange={checked => {
-              setCurrentModel(prev => ({
+            field='priceMode'
+            label={
+              <>
+                {t('定价模式')}:
+                {currentModel?.priceMode ? t('固定价格') : t('倍率模式')}
+              </>
+            }
+            onChange={(checked) => {
+              setCurrentModel((prev) => ({
                 ...prev,
                 price: '',
                 ratio: '',
                 completionRatio: '',
-                priceMode: checked
+                priceMode: checked,
               }));
             }}
           />
           {currentModel?.priceMode ? (
             <Form.Input
-              field="price"
+              field='price'
               label={t('固定价格(每次)')}
               placeholder={t('输入每次价格')}
-              onChange={value => setCurrentModel(prev => ({ ...prev, price: value }))}
+              onChange={(value) =>
+                setCurrentModel((prev) => ({ ...prev, price: value }))
+              }
             />
           ) : (
             <>
               <Form.Input
-                field="ratio"
+                field='ratio'
                 label={t('模型倍率')}
                 placeholder={t('输入模型倍率')}
-                onChange={value => setCurrentModel(prev => ({ ...prev, ratio: value }))}
+                onChange={(value) =>
+                  setCurrentModel((prev) => ({ ...prev, ratio: value }))
+                }
               />
               <Form.Input
-                field="completionRatio"
+                field='completionRatio'
                 label={t('补全倍率')}
                 placeholder={t('输入补全价格')}
-                onChange={value => setCurrentModel(prev => ({ ...prev, completionRatio: value }))}
+                onChange={(value) =>
+                  setCurrentModel((prev) => ({
+                    ...prev,
+                    completionRatio: value,
+                  }))
+                }
               />
             </>
           )}
@@ -496,50 +549,56 @@ export default function ModelRatioNotSetEditor(props) {
               </Space>
             </div>
           </Form.Section>
-          
+
           {batchFillType === 'bothRatio' ? (
             <>
               <Form.Input
-                field="batchRatioValue"
+                field='batchRatioValue'
                 label={t('模型倍率值')}
                 placeholder={t('请输入模型倍率')}
                 value={batchRatioValue}
-                onChange={value => setBatchRatioValue(value)}
+                onChange={(value) => setBatchRatioValue(value)}
               />
               <Form.Input
-                field="batchCompletionRatioValue"
+                field='batchCompletionRatioValue'
                 label={t('补全倍率值')}
                 placeholder={t('请输入补全倍率')}
                 value={batchCompletionRatioValue}
-                onChange={value => setBatchCompletionRatioValue(value)}
+                onChange={(value) => setBatchCompletionRatioValue(value)}
               />
             </>
           ) : (
             <Form.Input
-              field="batchFillValue"
+              field='batchFillValue'
               label={
-                batchFillType === 'price' 
-                  ? t('固定价格值') 
+                batchFillType === 'price'
+                  ? t('固定价格值')
                   : batchFillType === 'ratio'
                     ? t('模型倍率值')
                     : t('补全倍率值')
               }
               placeholder={t('请输入数值')}
               value={batchFillValue}
-              onChange={value => setBatchFillValue(value)}
+              onChange={(value) => setBatchFillValue(value)}
             />
           )}
-          
-          <Text type="tertiary">
-            {t('将为选中的 ')} <Text strong>{selectedRowKeys.length}</Text> {t(' 个模型设置相同的值')}
+
+          <Text type='tertiary'>
+            {t('将为选中的 ')} <Text strong>{selectedRowKeys.length}</Text>{' '}
+            {t(' 个模型设置相同的值')}
           </Text>
           <div style={{ marginTop: '8px' }}>
-            <Text type="tertiary">
-              {t('当前设置类型: ')} <Text strong>{
-                batchFillType === 'price' ? t('固定价格') : 
-                batchFillType === 'ratio' ? t('模型倍率') : 
-                batchFillType === 'completionRatio' ? t('补全倍率') : t('模型倍率和补全倍率')
-              }</Text>
+            <Text type='tertiary'>
+              {t('当前设置类型: ')}{' '}
+              <Text strong>
+                {batchFillType === 'price'
+                  ? t('固定价格')
+                  : batchFillType === 'ratio'
+                    ? t('模型倍率')
+                    : batchFillType === 'completionRatio'
+                      ? t('补全倍率')
+                      : t('模型倍率和补全倍率')}
+              </Text>
             </Text>
           </div>
         </Form>

+ 294 - 198
web/src/pages/Setting/Operation/ModelSettingsVisualEditor.js

@@ -1,7 +1,24 @@
 // ModelSettingsVisualEditor.js
 import React, { useContext, useEffect, useState, useRef } from 'react';
-import { Table, Button, Input, Modal, Form, Space, RadioGroup, Radio, Tabs, TabPane } from '@douyinfe/semi-ui';
-import { IconDelete, IconPlus, IconSearch, IconSave, IconEdit } from '@douyinfe/semi-icons';
+import {
+  Table,
+  Button,
+  Input,
+  Modal,
+  Form,
+  Space,
+  RadioGroup,
+  Radio,
+  Tabs,
+  TabPane,
+} from '@douyinfe/semi-ui';
+import {
+  IconDelete,
+  IconPlus,
+  IconSearch,
+  IconSave,
+  IconEdit,
+} from '@douyinfe/semi-icons';
 import { showError, showSuccess } from '../../../helpers';
 import { API } from '../../../helpers';
 import { useTranslation } from 'react-i18next';
@@ -20,7 +37,7 @@ export default function ModelSettingsVisualEditor(props) {
   const [pricingSubMode, setPricingSubMode] = useState('ratio'); // 'ratio' or 'token-price'
   const formRef = useRef(null);
   const pageSize = 10;
-  const quotaPerUnit = getQuotaPerUnit()
+  const quotaPerUnit = getQuotaPerUnit();
 
   useEffect(() => {
     try {
@@ -32,14 +49,15 @@ export default function ModelSettingsVisualEditor(props) {
       const modelNames = new Set([
         ...Object.keys(modelPrice),
         ...Object.keys(modelRatio),
-        ...Object.keys(completionRatio)
+        ...Object.keys(completionRatio),
       ]);
 
-      const modelData = Array.from(modelNames).map(name => ({
+      const modelData = Array.from(modelNames).map((name) => ({
         name,
         price: modelPrice[name] === undefined ? '' : modelPrice[name],
         ratio: modelRatio[name] === undefined ? '' : modelRatio[name],
-        completionRatio: completionRatio[name] === undefined ? '' : completionRatio[name]
+        completionRatio:
+          completionRatio[name] === undefined ? '' : completionRatio[name],
       }));
 
       setModels(modelData);
@@ -56,8 +74,10 @@ export default function ModelSettingsVisualEditor(props) {
   };
 
   // 在 return 语句之前,先处理过滤和分页逻辑
-  const filteredModels = models.filter(model =>
-    searchText ? model.name.toLowerCase().includes(searchText.toLowerCase()) : true
+  const filteredModels = models.filter((model) =>
+    searchText
+      ? model.name.toLowerCase().includes(searchText.toLowerCase())
+      : true,
   );
 
   // 然后基于过滤后的数据计算分页数据
@@ -68,20 +88,24 @@ export default function ModelSettingsVisualEditor(props) {
     const output = {
       ModelPrice: {},
       ModelRatio: {},
-      CompletionRatio: {}
+      CompletionRatio: {},
     };
     let currentConvertModelName = '';
 
     try {
       // 数据转换
-      models.forEach(model => {
+      models.forEach((model) => {
         currentConvertModelName = model.name;
         if (model.price !== '') {
           // 如果价格不为空,则转换为浮点数,忽略倍率参数
-          output.ModelPrice[model.name] = parseFloat(model.price)
+          output.ModelPrice[model.name] = parseFloat(model.price);
         } else {
-          if (model.ratio !== '') output.ModelRatio[model.name] = parseFloat(model.ratio);
-          if (model.completionRatio !== '') output.CompletionRatio[model.name] = parseFloat(model.completionRatio);
+          if (model.ratio !== '')
+            output.ModelRatio[model.name] = parseFloat(model.ratio);
+          if (model.completionRatio !== '')
+            output.CompletionRatio[model.name] = parseFloat(
+              model.completionRatio,
+            );
         }
       });
 
@@ -89,13 +113,13 @@ export default function ModelSettingsVisualEditor(props) {
       const finalOutput = {
         ModelPrice: JSON.stringify(output.ModelPrice, null, 2),
         ModelRatio: JSON.stringify(output.ModelRatio, null, 2),
-        CompletionRatio: JSON.stringify(output.CompletionRatio, null, 2)
+        CompletionRatio: JSON.stringify(output.CompletionRatio, null, 2),
       };
 
       const requestQueue = Object.entries(finalOutput).map(([key, value]) => {
         return API.put('/api/option/', {
           key,
-          value
+          value,
         });
       });
 
@@ -120,7 +144,6 @@ export default function ModelSettingsVisualEditor(props) {
 
       showSuccess('保存成功');
       props.refresh();
-
     } catch (error) {
       console.error('保存失败:', error);
       showError('保存失败,请重试');
@@ -143,9 +166,9 @@ export default function ModelSettingsVisualEditor(props) {
         <Input
           value={text}
           placeholder={t('按量计费')}
-          onChange={value => updateModel(record.name, 'price', value)}
+          onChange={(value) => updateModel(record.name, 'price', value)}
         />
-      )
+      ),
     },
     {
       title: t('模型倍率'),
@@ -156,9 +179,9 @@ export default function ModelSettingsVisualEditor(props) {
           value={text}
           placeholder={record.price !== '' ? t('模型倍率') : t('默认补全倍率')}
           disabled={record.price !== ''}
-          onChange={value => updateModel(record.name, 'ratio', value)}
+          onChange={(value) => updateModel(record.name, 'ratio', value)}
         />
-      )
+      ),
     },
     {
       title: t('补全倍率'),
@@ -169,9 +192,11 @@ export default function ModelSettingsVisualEditor(props) {
           value={text}
           placeholder={record.price !== '' ? t('补全倍率') : t('默认补全倍率')}
           disabled={record.price !== ''}
-          onChange={value => updateModel(record.name, 'completionRatio', value)}
+          onChange={(value) =>
+            updateModel(record.name, 'completionRatio', value)
+          }
         />
-      )
+      ),
     },
     {
       title: t('操作'),
@@ -179,19 +204,18 @@ export default function ModelSettingsVisualEditor(props) {
       render: (_, record) => (
         <Space>
           <Button
-            type="primary"
+            type='primary'
             icon={<IconEdit />}
             onClick={() => editModel(record)}
-          >
-          </Button>
+          ></Button>
           <Button
             icon={<IconDelete />}
-            type="danger"
+            type='danger'
             onClick={() => deleteModel(record.name)}
           />
         </Space>
-      )
-    }
+      ),
+    },
   ];
 
   const updateModel = (name, field, value) => {
@@ -199,103 +223,114 @@ export default function ModelSettingsVisualEditor(props) {
       showError('请输入数字');
       return;
     }
-    setModels(prev =>
-      prev.map(model =>
-        model.name === name
-          ? { ...model, [field]: value }
-          : model
-      )
+    setModels((prev) =>
+      prev.map((model) =>
+        model.name === name ? { ...model, [field]: value } : model,
+      ),
     );
   };
 
   const deleteModel = (name) => {
-    setModels(prev => prev.filter(model => model.name !== name));
+    setModels((prev) => prev.filter((model) => model.name !== name));
   };
-  
+
   const calculateRatioFromTokenPrice = (tokenPrice) => {
     return tokenPrice / 2;
   };
-  
-  const calculateCompletionRatioFromPrices = (modelTokenPrice, completionTokenPrice) => {
+
+  const calculateCompletionRatioFromPrices = (
+    modelTokenPrice,
+    completionTokenPrice,
+  ) => {
     if (!modelTokenPrice || modelTokenPrice === '0') {
       showError('模型价格不能为0');
       return '';
     }
     return completionTokenPrice / modelTokenPrice;
   };
-  
-  const handleTokenPriceChange = (value) => {
 
+  const handleTokenPriceChange = (value) => {
     // Use a temporary variable to hold the new state
     let newState = {
       ...(currentModel || {}),
       tokenPrice: value,
-      ratio: 0
+      ratio: 0,
     };
-    
+
     if (!isNaN(value) && value !== '') {
       const tokenPrice = parseFloat(value);
       const ratio = calculateRatioFromTokenPrice(tokenPrice);
       newState.ratio = ratio;
     }
-    
+
     // Set the state with the complete updated object
     setCurrentModel(newState);
   };
-  
-  const handleCompletionTokenPriceChange = (value) => {
 
+  const handleCompletionTokenPriceChange = (value) => {
     // Use a temporary variable to hold the new state
     let newState = {
       ...(currentModel || {}),
       completionTokenPrice: value,
-      completionRatio: 0
+      completionRatio: 0,
     };
-    
+
     if (!isNaN(value) && value !== '' && currentModel?.tokenPrice) {
       const completionTokenPrice = parseFloat(value);
       const modelTokenPrice = parseFloat(currentModel.tokenPrice);
-      
+
       if (modelTokenPrice > 0) {
-        const completionRatio = calculateCompletionRatioFromPrices(modelTokenPrice, completionTokenPrice);
+        const completionRatio = calculateCompletionRatioFromPrices(
+          modelTokenPrice,
+          completionTokenPrice,
+        );
         newState.completionRatio = completionRatio;
       }
     }
-    
+
     // Set the state with the complete updated object
     setCurrentModel(newState);
   };
 
   const addOrUpdateModel = (values) => {
     // Check if we're editing an existing model or adding a new one
-    const existingModelIndex = models.findIndex(model => model.name === values.name);
-    
+    const existingModelIndex = models.findIndex(
+      (model) => model.name === values.name,
+    );
+
     if (existingModelIndex >= 0) {
       // Update existing model
-      setModels(prev => prev.map((model, index) => 
-        index === existingModelIndex ? {
-          name: values.name,
-          price: values.price || '',
-          ratio: values.ratio || '',
-          completionRatio: values.completionRatio || ''
-        } : model
-      ));
+      setModels((prev) =>
+        prev.map((model, index) =>
+          index === existingModelIndex
+            ? {
+                name: values.name,
+                price: values.price || '',
+                ratio: values.ratio || '',
+                completionRatio: values.completionRatio || '',
+              }
+            : model,
+        ),
+      );
       setVisible(false);
       showSuccess(t('更新成功'));
     } else {
       // Add new model
       // Check if model name already exists
-      if (models.some(model => model.name === values.name)) {
+      if (models.some((model) => model.name === values.name)) {
         showError(t('模型名称已存在'));
         return;
       }
-      
-      setModels(prev => [{
-        name: values.name,
-        price: values.price || '',
-        ratio: values.ratio || '',
-        completionRatio: values.completionRatio || ''
-      }, ...prev]);
+
+      setModels((prev) => [
+        {
+          name: values.name,
+          price: values.price || '',
+          ratio: values.ratio || '',
+          completionRatio: values.completionRatio || '',
+        },
+        ...prev,
+      ]);
       setVisible(false);
       showSuccess(t('添加成功'));
     }
@@ -304,7 +339,7 @@ export default function ModelSettingsVisualEditor(props) {
   const calculateTokenPriceFromRatio = (ratio) => {
     return ratio * 2;
   };
-  
+
   const resetModalState = () => {
     setCurrentModel(null);
     setPricingMode('per-token');
@@ -312,40 +347,43 @@ export default function ModelSettingsVisualEditor(props) {
   };
 
   const editModel = (record) => {
-
     // Determine which pricing mode to use based on the model's current configuration
     let initialPricingMode = 'per-token';
     let initialPricingSubMode = 'ratio';
-    
+
     if (record.price !== '') {
       initialPricingMode = 'per-request';
     } else {
       initialPricingMode = 'per-token';
       // We default to ratio mode, but could set to token-price if needed
     }
-    
+
     // Set the pricing modes for the form
     setPricingMode(initialPricingMode);
     setPricingSubMode(initialPricingSubMode);
-    
+
     // Create a copy of the model data to avoid modifying the original
     const modelCopy = { ...record };
-    
+
     // If the model has ratio data and we want to populate token price fields
     if (record.ratio) {
-      modelCopy.tokenPrice = calculateTokenPriceFromRatio(parseFloat(record.ratio)).toString();
-      
+      modelCopy.tokenPrice = calculateTokenPriceFromRatio(
+        parseFloat(record.ratio),
+      ).toString();
+
       if (record.completionRatio) {
-        modelCopy.completionTokenPrice = (parseFloat(modelCopy.tokenPrice) * parseFloat(record.completionRatio)).toString();
+        modelCopy.completionTokenPrice = (
+          parseFloat(modelCopy.tokenPrice) * parseFloat(record.completionRatio)
+        ).toString();
       }
     }
-    
+
     // Set the current model
     setCurrentModel(modelCopy);
-    
+
     // Open the modal
     setVisible(true);
-    
+
     // Use setTimeout to ensure the form is rendered before setting values
     setTimeout(() => {
       if (formRef.current) {
@@ -353,7 +391,7 @@ export default function ModelSettingsVisualEditor(props) {
         const formValues = {
           name: modelCopy.name,
         };
-        
+
         if (initialPricingMode === 'per-request') {
           formValues.priceInput = modelCopy.price;
         } else if (initialPricingMode === 'per-token') {
@@ -362,7 +400,7 @@ export default function ModelSettingsVisualEditor(props) {
           formValues.modelTokenPrice = modelCopy.tokenPrice;
           formValues.completionTokenPrice = modelCopy.completionTokenPrice;
         }
-        
+
         formRef.current.setValues(formValues);
       }
     }, 0);
@@ -370,23 +408,26 @@ export default function ModelSettingsVisualEditor(props) {
 
   return (
     <>
-      <Space vertical align="start" style={{ width: '100%' }}>
+      <Space vertical align='start' style={{ width: '100%' }}>
         <Space>
-          <Button icon={<IconPlus />} onClick={() => {
-            resetModalState();
-            setVisible(true);
-          }}>
+          <Button
+            icon={<IconPlus />}
+            onClick={() => {
+              resetModalState();
+              setVisible(true);
+            }}
+          >
             {t('添加模型')}
           </Button>
-          <Button type="primary" icon={<IconSave />} onClick={SubmitData}>
+          <Button type='primary' icon={<IconSave />} onClick={SubmitData}>
             {t('应用更改')}
           </Button>
           <Input
             prefix={<IconSearch />}
             placeholder={t('搜索模型名称')}
             value={searchText}
-            onChange={value => {
-              setSearchText(value)
+            onChange={(value) => {
+              setSearchText(value);
               setCurrentPage(1);
             }}
             style={{ width: 200 }}
@@ -399,21 +440,27 @@ export default function ModelSettingsVisualEditor(props) {
             currentPage: currentPage,
             pageSize: pageSize,
             total: filteredModels.length,
-            onPageChange: page => setCurrentPage(page),
+            onPageChange: (page) => setCurrentPage(page),
             formatPageText: (page) =>
               t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
                 start: page.currentStart,
                 end: page.currentEnd,
-                total: filteredModels.length
+                total: filteredModels.length,
               }),
             showTotal: true,
-            showSizeChanger: false
+            showSizeChanger: false,
           }}
         />
       </Space>
 
       <Modal
-        title={currentModel && currentModel.name && models.some(model => model.name === currentModel.name) ? t('编辑模型') : t('添加模型')}
+        title={
+          currentModel &&
+          currentModel.name &&
+          models.some((model) => model.name === currentModel.name)
+            ? t('编辑模型')
+            : t('添加模型')
+        }
         visible={visible}
         onCancel={() => {
           resetModalState();
@@ -423,22 +470,33 @@ export default function ModelSettingsVisualEditor(props) {
           if (currentModel) {
             // If we're in token price mode, make sure ratio values are properly set
             const valuesToSave = { ...currentModel };
-            
-            if (pricingMode === 'per-token' && pricingSubMode === 'token-price' && currentModel.tokenPrice) {
+
+            if (
+              pricingMode === 'per-token' &&
+              pricingSubMode === 'token-price' &&
+              currentModel.tokenPrice
+            ) {
               // Calculate and set ratio from token price
               const tokenPrice = parseFloat(currentModel.tokenPrice);
               valuesToSave.ratio = (tokenPrice / 2).toString();
-              
+
               // Calculate and set completion ratio if both token prices are available
-              if (currentModel.completionTokenPrice && currentModel.tokenPrice) {
-                const completionPrice = parseFloat(currentModel.completionTokenPrice);
+              if (
+                currentModel.completionTokenPrice &&
+                currentModel.tokenPrice
+              ) {
+                const completionPrice = parseFloat(
+                  currentModel.completionTokenPrice,
+                );
                 const modelPrice = parseFloat(currentModel.tokenPrice);
                 if (modelPrice > 0) {
-                  valuesToSave.completionRatio = (completionPrice / modelPrice).toString();
+                  valuesToSave.completionRatio = (
+                    completionPrice / modelPrice
+                  ).toString();
                 }
               }
             }
-            
+
             // Clear price if we're in per-token mode
             if (pricingMode === 'per-token') {
               valuesToSave.price = '';
@@ -447,139 +505,175 @@ export default function ModelSettingsVisualEditor(props) {
               valuesToSave.ratio = '';
               valuesToSave.completionRatio = '';
             }
-            
+
             addOrUpdateModel(valuesToSave);
           }
         }}
       >
-        <Form getFormApi={api => formRef.current = api}>
+        <Form getFormApi={(api) => (formRef.current = api)}>
           <Form.Input
-            field="name"
+            field='name'
             label={t('模型名称')}
-            placeholder="strawberry"
+            placeholder='strawberry'
             required
-            disabled={currentModel && currentModel.name && models.some(model => model.name === currentModel.name)}
-            onChange={value => setCurrentModel(prev => ({ ...prev, name: value }))}
+            disabled={
+              currentModel &&
+              currentModel.name &&
+              models.some((model) => model.name === currentModel.name)
+            }
+            onChange={(value) =>
+              setCurrentModel((prev) => ({ ...prev, name: value }))
+            }
           />
-          
+
           <Form.Section text={t('定价模式')}>
             <div style={{ marginBottom: '16px' }}>
-              <RadioGroup type="button" value={pricingMode} onChange={(e) => {
-                const newMode = e.target.value;
-                const oldMode = pricingMode;
-                setPricingMode(newMode);
-                
-                // Instead of resetting all values, convert between modes
-                if (currentModel) {
-                  const updatedModel = { ...currentModel };
-                  
-                  // Update formRef with converted values
-                  if (formRef.current) {
-                    const formValues = {
-                      name: updatedModel.name
-                    };
-                    
-                    if (newMode === 'per-request') {
-                      formValues.priceInput = updatedModel.price || '';
-                    } else if (newMode === 'per-token') {
-                      formValues.ratioInput = updatedModel.ratio || '';
-                      formValues.completionRatioInput = updatedModel.completionRatio || '';
-                      formValues.modelTokenPrice = updatedModel.tokenPrice || '';
-                      formValues.completionTokenPrice = updatedModel.completionTokenPrice || '';
+              <RadioGroup
+                type='button'
+                value={pricingMode}
+                onChange={(e) => {
+                  const newMode = e.target.value;
+                  const oldMode = pricingMode;
+                  setPricingMode(newMode);
+
+                  // Instead of resetting all values, convert between modes
+                  if (currentModel) {
+                    const updatedModel = { ...currentModel };
+
+                    // Update formRef with converted values
+                    if (formRef.current) {
+                      const formValues = {
+                        name: updatedModel.name,
+                      };
+
+                      if (newMode === 'per-request') {
+                        formValues.priceInput = updatedModel.price || '';
+                      } else if (newMode === 'per-token') {
+                        formValues.ratioInput = updatedModel.ratio || '';
+                        formValues.completionRatioInput =
+                          updatedModel.completionRatio || '';
+                        formValues.modelTokenPrice =
+                          updatedModel.tokenPrice || '';
+                        formValues.completionTokenPrice =
+                          updatedModel.completionTokenPrice || '';
+                      }
+
+                      formRef.current.setValues(formValues);
                     }
-                    
-                    formRef.current.setValues(formValues);
+
+                    // Update the model state
+                    setCurrentModel(updatedModel);
                   }
-                  
-                  // Update the model state
-                  setCurrentModel(updatedModel);
-                }
-              }}>
-                <Radio value="per-token">{t('按量计费')}</Radio>
-                <Radio value="per-request">{t('按次计费')}</Radio>
+                }}
+              >
+                <Radio value='per-token'>{t('按量计费')}</Radio>
+                <Radio value='per-request'>{t('按次计费')}</Radio>
               </RadioGroup>
             </div>
           </Form.Section>
-          
+
           {pricingMode === 'per-token' && (
             <>
               <Form.Section text={t('价格设置方式')}>
                 <div style={{ marginBottom: '16px' }}>
-                  <RadioGroup type="button" value={pricingSubMode} onChange={(e) => {
-                    const newSubMode = e.target.value;
-                    const oldSubMode = pricingSubMode;
-                    setPricingSubMode(newSubMode);
-                    
-                    // Handle conversion between submodes
-                    if (currentModel) {
-                      const updatedModel = { ...currentModel };
-                      
-                      // Convert between ratio and token price
-                      if (oldSubMode === 'ratio' && newSubMode === 'token-price') {
-                        if (updatedModel.ratio) {
-                          updatedModel.tokenPrice = calculateTokenPriceFromRatio(parseFloat(updatedModel.ratio)).toString();
-                          
-                          if (updatedModel.completionRatio) {
-                            updatedModel.completionTokenPrice = (parseFloat(updatedModel.tokenPrice) * parseFloat(updatedModel.completionRatio)).toString();
+                  <RadioGroup
+                    type='button'
+                    value={pricingSubMode}
+                    onChange={(e) => {
+                      const newSubMode = e.target.value;
+                      const oldSubMode = pricingSubMode;
+                      setPricingSubMode(newSubMode);
+
+                      // Handle conversion between submodes
+                      if (currentModel) {
+                        const updatedModel = { ...currentModel };
+
+                        // Convert between ratio and token price
+                        if (
+                          oldSubMode === 'ratio' &&
+                          newSubMode === 'token-price'
+                        ) {
+                          if (updatedModel.ratio) {
+                            updatedModel.tokenPrice =
+                              calculateTokenPriceFromRatio(
+                                parseFloat(updatedModel.ratio),
+                              ).toString();
+
+                            if (updatedModel.completionRatio) {
+                              updatedModel.completionTokenPrice = (
+                                parseFloat(updatedModel.tokenPrice) *
+                                parseFloat(updatedModel.completionRatio)
+                              ).toString();
+                            }
                           }
+                        } else if (
+                          oldSubMode === 'token-price' &&
+                          newSubMode === 'ratio'
+                        ) {
+                          // Ratio values should already be calculated by the handlers
                         }
-                      } else if (oldSubMode === 'token-price' && newSubMode === 'ratio') {
-                        // Ratio values should already be calculated by the handlers
-                      }
-                      
-                      // Update the form values
-                      if (formRef.current) {
-                        const formValues = {};
-                        
-                        if (newSubMode === 'ratio') {
-                          formValues.ratioInput = updatedModel.ratio || '';
-                          formValues.completionRatioInput = updatedModel.completionRatio || '';
-                        } else if (newSubMode === 'token-price') {
-                          formValues.modelTokenPrice = updatedModel.tokenPrice || '';
-                          formValues.completionTokenPrice = updatedModel.completionTokenPrice || '';
+
+                        // Update the form values
+                        if (formRef.current) {
+                          const formValues = {};
+
+                          if (newSubMode === 'ratio') {
+                            formValues.ratioInput = updatedModel.ratio || '';
+                            formValues.completionRatioInput =
+                              updatedModel.completionRatio || '';
+                          } else if (newSubMode === 'token-price') {
+                            formValues.modelTokenPrice =
+                              updatedModel.tokenPrice || '';
+                            formValues.completionTokenPrice =
+                              updatedModel.completionTokenPrice || '';
+                          }
+
+                          formRef.current.setValues(formValues);
                         }
-                        
-                        formRef.current.setValues(formValues);
+
+                        setCurrentModel(updatedModel);
                       }
-                      
-                      setCurrentModel(updatedModel);
-                    }
-                  }}>
-                    <Radio value="ratio">{t('按倍率设置')}</Radio>
-                    <Radio value="token-price">{t('按价格设置')}</Radio>
+                    }}
+                  >
+                    <Radio value='ratio'>{t('按倍率设置')}</Radio>
+                    <Radio value='token-price'>{t('按价格设置')}</Radio>
                   </RadioGroup>
                 </div>
               </Form.Section>
-              
+
               {pricingSubMode === 'ratio' && (
                 <>
                   <Form.Input
-                    field="ratioInput"
+                    field='ratioInput'
                     label={t('模型倍率')}
                     placeholder={t('输入模型倍率')}
-                    onChange={value => setCurrentModel(prev => ({ 
-                      ...prev || {}, 
-                      ratio: value 
-                    }))}
+                    onChange={(value) =>
+                      setCurrentModel((prev) => ({
+                        ...(prev || {}),
+                        ratio: value,
+                      }))
+                    }
                     initValue={currentModel?.ratio || ''}
                   />
                   <Form.Input
-                    field="completionRatioInput"
+                    field='completionRatioInput'
                     label={t('补全倍率')}
                     placeholder={t('输入补全倍率')}
-                    onChange={value => setCurrentModel(prev => ({ 
-                      ...prev || {}, 
-                      completionRatio: value 
-                    }))}
+                    onChange={(value) =>
+                      setCurrentModel((prev) => ({
+                        ...(prev || {}),
+                        completionRatio: value,
+                      }))
+                    }
                     initValue={currentModel?.completionRatio || ''}
                   />
                 </>
               )}
-              
+
               {pricingSubMode === 'token-price' && (
                 <>
                   <Form.Input
-                    field="modelTokenPrice"
+                    field='modelTokenPrice'
                     label={t('输入价格')}
                     onChange={(value) => {
                       handleTokenPriceChange(value);
@@ -588,7 +682,7 @@ export default function ModelSettingsVisualEditor(props) {
                     suffix={t('$/1M tokens')}
                   />
                   <Form.Input
-                    field="completionTokenPrice"
+                    field='completionTokenPrice'
                     label={t('输出价格')}
                     onChange={(value) => {
                       handleCompletionTokenPriceChange(value);
@@ -600,16 +694,18 @@ export default function ModelSettingsVisualEditor(props) {
               )}
             </>
           )}
-          
+
           {pricingMode === 'per-request' && (
             <Form.Input
-              field="priceInput"
+              field='priceInput'
               label={t('固定价格(每次)')}
               placeholder={t('输入每次价格')}
-              onChange={value => setCurrentModel(prev => ({ 
-                ...prev || {}, 
-                price: value 
-              }))}
+              onChange={(value) =>
+                setCurrentModel((prev) => ({
+                  ...(prev || {}),
+                  price: value,
+                }))
+              }
               initValue={currentModel?.price || ''}
             />
           )}

+ 62 - 47
web/src/pages/Setting/Operation/SettingsChats.js

@@ -1,5 +1,14 @@
 import React, { useEffect, useState, useRef } from 'react';
-import { Banner, Button, Col, Form, Popconfirm, Row, Space, Spin } from '@douyinfe/semi-ui';
+import {
+  Banner,
+  Button,
+  Col,
+  Form,
+  Popconfirm,
+  Row,
+  Space,
+  Spin,
+} from '@douyinfe/semi-ui';
 import {
   compareObjects,
   API,
@@ -7,7 +16,7 @@ import {
   showSuccess,
   showWarning,
   verifyJSON,
-  verifyJSONPromise
+  verifyJSONPromise,
 } from '../../../helpers';
 import { useTranslation } from 'react-i18next';
 
@@ -15,7 +24,7 @@ export default function SettingsChats(props) {
   const { t } = useTranslation();
   const [loading, setLoading] = useState(false);
   const [inputs, setInputs] = useState({
-    Chats: "[]",
+    Chats: '[]',
   });
   const refForm = useRef();
   const [inputsRow, setInputsRow] = useState(inputs);
@@ -23,44 +32,48 @@ export default function SettingsChats(props) {
   async function onSubmit() {
     try {
       console.log('Starting validation...');
-      await refForm.current.validate().then(() => {
-        console.log('Validation passed');
-        const updateArray = compareObjects(inputs, inputsRow);
-        if (!updateArray.length) return showWarning(t('你似乎并没有修改什么'));
-        const requestQueue = updateArray.map((item) => {
-          let value = '';
-          if (typeof inputs[item.key] === 'boolean') {
-            value = String(inputs[item.key]);
-          } else {
-            value = inputs[item.key];
-          }
-          return API.put('/api/option/', {
-            key: item.key,
-            value
-          });
-        });
-        setLoading(true);
-        Promise.all(requestQueue)
-          .then((res) => {
-            if (requestQueue.length === 1) {
-              if (res.includes(undefined)) return;
-            } else if (requestQueue.length > 1) {
-              if (res.includes(undefined))
-                return showError(t('部分保存失败,请重试'));
+      await refForm.current
+        .validate()
+        .then(() => {
+          console.log('Validation passed');
+          const updateArray = compareObjects(inputs, inputsRow);
+          if (!updateArray.length)
+            return showWarning(t('你似乎并没有修改什么'));
+          const requestQueue = updateArray.map((item) => {
+            let value = '';
+            if (typeof inputs[item.key] === 'boolean') {
+              value = String(inputs[item.key]);
+            } else {
+              value = inputs[item.key];
             }
-            showSuccess(t('保存成功'));
-            props.refresh();
-          })
-          .catch(() => {
-            showError(t('保存失败,请重试'));
-          })
-          .finally(() => {
-            setLoading(false);
+            return API.put('/api/option/', {
+              key: item.key,
+              value,
+            });
           });
-      }).catch((error) => {
-        console.error('Validation failed:', error);
-        showError(t('请检查输入'));
-      });
+          setLoading(true);
+          Promise.all(requestQueue)
+            .then((res) => {
+              if (requestQueue.length === 1) {
+                if (res.includes(undefined)) return;
+              } else if (requestQueue.length > 1) {
+                if (res.includes(undefined))
+                  return showError(t('部分保存失败,请重试'));
+              }
+              showSuccess(t('保存成功'));
+              props.refresh();
+            })
+            .catch(() => {
+              showError(t('保存失败,请重试'));
+            })
+            .finally(() => {
+              setLoading(false);
+            });
+        })
+        .catch((error) => {
+          console.error('Validation failed:', error);
+          showError(t('请检查输入'));
+        });
     } catch (error) {
       showError(t('请检查输入'));
       console.error(error);
@@ -109,11 +122,15 @@ export default function SettingsChats(props) {
         <Form.Section text={t('令牌聊天设置')}>
           <Banner
             type='warning'
-            description={t('必须将上方聊天链接全部设置为空,才能使用下方聊天设置功能')}
+            description={t(
+              '必须将上方聊天链接全部设置为空,才能使用下方聊天设置功能',
+            )}
           />
           <Banner
             type='info'
-            description={t('链接中的{key}将自动替换为sk-xxxx,{address}将自动替换为系统设置的服务器地址,末尾不带/和/v1')}
+            description={t(
+              '链接中的{key}将自动替换为sk-xxxx,{address}将自动替换为系统设置的服务器地址,末尾不带/和/v1',
+            )}
           />
           <Form.TextArea
             label={t('聊天配置')}
@@ -128,22 +145,20 @@ export default function SettingsChats(props) {
                 validator: (rule, value) => {
                   return verifyJSON(value);
                 },
-                message: t('不是合法的 JSON 字符串')
-              }
+                message: t('不是合法的 JSON 字符串'),
+              },
             ]}
             onChange={(value) =>
               setInputs({
                 ...inputs,
-                Chats: value
+                Chats: value,
               })
             }
           />
         </Form.Section>
       </Form>
       <Space>
-        <Button onClick={onSubmit}>
-          {t('保存聊天设置')}
-        </Button>
+        <Button onClick={onSubmit}>{t('保存聊天设置')}</Button>
       </Space>
     </Spin>
   );

+ 2 - 1
web/src/pages/Setting/Operation/SettingsCreditLimit.js

@@ -42,7 +42,8 @@ export default function SettingsCreditLimit(props) {
         if (requestQueue.length === 1) {
           if (res.includes(undefined)) return;
         } else if (requestQueue.length > 1) {
-          if (res.includes(undefined)) return showError(t('部分保存失败,请重试'));
+          if (res.includes(undefined))
+            return showError(t('部分保存失败,请重试'));
         }
         showSuccess(t('保存成功'));
         props.refresh();

+ 3 - 2
web/src/pages/Setting/Operation/SettingsDataDashboard.js

@@ -11,7 +11,7 @@ import { useTranslation } from 'react-i18next';
 
 export default function DataDashboard(props) {
   const { t } = useTranslation();
-  
+
   const optionsDataExportDefaultTime = [
     { key: 'hour', label: t('小时'), value: 'hour' },
     { key: 'day', label: t('天'), value: 'day' },
@@ -47,7 +47,8 @@ export default function DataDashboard(props) {
         if (requestQueue.length === 1) {
           if (res.includes(undefined)) return;
         } else if (requestQueue.length > 1) {
-          if (res.includes(undefined)) return showError(t('部分保存失败,请重试'));
+          if (res.includes(undefined))
+            return showError(t('部分保存失败,请重试'));
         }
         showSuccess(t('保存成功'));
         props.refresh();

+ 4 - 2
web/src/pages/Setting/Operation/SettingsDrawing.js

@@ -44,7 +44,8 @@ export default function SettingsDrawing(props) {
         if (requestQueue.length === 1) {
           if (res.includes(undefined)) return;
         } else if (requestQueue.length > 1) {
-          if (res.includes(undefined)) return showError(t('部分保存失败,请重试'));
+          if (res.includes(undefined))
+            return showError(t('部分保存失败,请重试'));
         }
         showSuccess(t('保存成功'));
         props.refresh();
@@ -146,7 +147,8 @@ export default function SettingsDrawing(props) {
                   label={
                     <>
                       {t('开启之后会清除用户提示词中的')} <Tag>--fast</Tag> 、
-                      <Tag>--relax</Tag> {t('以及')} <Tag>--turbo</Tag> {t('参数')}
+                      <Tag>--relax</Tag> {t('以及')} <Tag>--turbo</Tag>{' '}
+                      {t('参数')}
                     </>
                   }
                   size='default'

+ 16 - 4
web/src/pages/Setting/Operation/SettingsGeneral.js

@@ -1,5 +1,14 @@
 import React, { useEffect, useState, useRef } from 'react';
-import { Banner, Button, Col, Form, Row, Spin, Collapse, Modal } from '@douyinfe/semi-ui';
+import {
+  Banner,
+  Button,
+  Col,
+  Form,
+  Row,
+  Spin,
+  Collapse,
+  Modal,
+} from '@douyinfe/semi-ui';
 import {
   compareObjects,
   API,
@@ -54,7 +63,8 @@ export default function GeneralSettings(props) {
         if (requestQueue.length === 1) {
           if (res.includes(undefined)) return;
         } else if (requestQueue.length > 1) {
-          if (res.includes(undefined)) return showError(t('部分保存失败,请重试'));
+          if (res.includes(undefined))
+            return showError(t('部分保存失败,请重试'));
         }
         showSuccess(t('保存成功'));
         props.refresh();
@@ -198,7 +208,7 @@ export default function GeneralSettings(props) {
           </Form.Section>
         </Form>
       </Spin>
-      
+
       <Modal
         title={t('警告')}
         visible={showQuotaWarning}
@@ -209,7 +219,9 @@ export default function GeneralSettings(props) {
       >
         <Banner
           type='warning'
-          description={t('此设置用于系统内部计算,默认值500000是为了精确到6位小数点设计,不推荐修改。')}
+          description={t(
+            '此设置用于系统内部计算,默认值500000是为了精确到6位小数点设计,不推荐修改。',
+          )}
           bordered
           fullMode={false}
           closeIcon={null}

+ 2 - 1
web/src/pages/Setting/Operation/SettingsLog.js

@@ -45,7 +45,8 @@ export default function SettingsLog(props) {
         if (requestQueue.length === 1) {
           if (res.includes(undefined)) return;
         } else if (requestQueue.length > 1) {
-          if (res.includes(undefined)) return showError(t('部分保存失败,请重试'));
+          if (res.includes(undefined))
+            return showError(t('部分保存失败,请重试'));
         }
         showSuccess(t('保存成功'));
         props.refresh();

+ 14 - 6
web/src/pages/Setting/Operation/SettingsMonitoring.js

@@ -5,7 +5,8 @@ import {
   API,
   showError,
   showSuccess,
-  showWarning, verifyJSON
+  showWarning,
+  verifyJSON,
 } from '../../../helpers';
 import { useTranslation } from 'react-i18next';
 
@@ -43,7 +44,8 @@ export default function SettingsMonitoring(props) {
         if (requestQueue.length === 1) {
           if (res.includes(undefined)) return;
         } else if (requestQueue.length > 1) {
-          if (res.includes(undefined)) return showError(t('部分保存失败,请重试'));
+          if (res.includes(undefined))
+            return showError(t('部分保存失败,请重试'));
         }
         showSuccess(t('保存成功'));
         props.refresh();
@@ -67,7 +69,7 @@ export default function SettingsMonitoring(props) {
     setInputsRow(structuredClone(currentInputs));
     refForm.current.setValues(currentInputs);
   }, [props.options]);
-  
+
   return (
     <>
       <Spin spinning={loading}>
@@ -84,7 +86,9 @@ export default function SettingsMonitoring(props) {
                   step={1}
                   min={0}
                   suffix={t('秒')}
-                  extraText={t('当运行通道全部测试时,超过此时间将自动禁用通道')}
+                  extraText={t(
+                    '当运行通道全部测试时,超过此时间将自动禁用通道',
+                  )}
                   placeholder={''}
                   field={'ChannelDisableThreshold'}
                   onChange={(value) =>
@@ -150,10 +154,14 @@ export default function SettingsMonitoring(props) {
                 <Form.TextArea
                   label={t('自动禁用关键词')}
                   placeholder={t('一行一个,不区分大小写')}
-                  extraText={t('当上游通道返回错误中包含这些关键词时(不区分大小写),自动禁用通道')}
+                  extraText={t(
+                    '当上游通道返回错误中包含这些关键词时(不区分大小写),自动禁用通道',
+                  )}
                   field={'AutomaticDisableKeywords'}
                   autosize={{ minRows: 6, maxRows: 12 }}
-                  onChange={(value) => setInputs({ ...inputs, AutomaticDisableKeywords: value })}
+                  onChange={(value) =>
+                    setInputs({ ...inputs, AutomaticDisableKeywords: value })
+                  }
                 />
               </Col>
             </Row>

+ 2 - 1
web/src/pages/Setting/Operation/SettingsSensitiveWords.js

@@ -41,7 +41,8 @@ export default function SettingsSensitiveWords(props) {
         if (requestQueue.length === 1) {
           if (res.includes(undefined)) return;
         } else if (requestQueue.length > 1) {
-          if (res.includes(undefined)) return showError(t('部分保存失败,请重试'));
+          if (res.includes(undefined))
+            return showError(t('部分保存失败,请重试'));
         }
         showSuccess(t('保存成功'));
         props.refresh();

+ 3 - 2
web/src/pages/Setting/RateLimit/SettingsRequestRateLimit.js

@@ -17,7 +17,7 @@ export default function RequestRateLimit(props) {
     ModelRequestRateLimitEnabled: false,
     ModelRequestRateLimitCount: -1,
     ModelRequestRateLimitSuccessCount: 1000,
-    ModelRequestRateLimitDurationMinutes: 1
+    ModelRequestRateLimitDurationMinutes: 1,
   });
   const refForm = useRef();
   const [inputsRow, setInputsRow] = useState(inputs);
@@ -43,7 +43,8 @@ export default function RequestRateLimit(props) {
         if (requestQueue.length === 1) {
           if (res.includes(undefined)) return;
         } else if (requestQueue.length > 1) {
-          if (res.includes(undefined)) return showError(t('部分保存失败,请重试'));
+          if (res.includes(undefined))
+            return showError(t('部分保存失败,请重试'));
         }
         showSuccess(t('保存成功'));
         props.refresh();

+ 105 - 57
web/src/pages/Setup/index.js

@@ -1,11 +1,27 @@
 import React, { useContext, useEffect, useState, useRef } from 'react';
-import { Card, Col, Row, Form, Button, Typography, Space, RadioGroup, Radio, Modal, Banner } from '@douyinfe/semi-ui';
+import {
+  Card,
+  Col,
+  Row,
+  Form,
+  Button,
+  Typography,
+  Space,
+  RadioGroup,
+  Radio,
+  Modal,
+  Banner,
+} from '@douyinfe/semi-ui';
 import { API, showError, showNotice, timestamp2string } from '../../helpers';
 import { StatusContext } from '../../context/Status';
 import { marked } from 'marked';
 import { StyleContext } from '../../context/Style/index.js';
 import { useTranslation } from 'react-i18next';
-import { IconHelpCircle, IconInfoCircle, IconAlertTriangle } from '@douyinfe/semi-icons';
+import {
+  IconHelpCircle,
+  IconInfoCircle,
+  IconAlertTriangle,
+} from '@douyinfe/semi-icons';
 
 const Setup = () => {
   const { t, i18n } = useTranslation();
@@ -16,16 +32,16 @@ const Setup = () => {
   const [setupStatus, setSetupStatus] = useState({
     status: false,
     root_init: false,
-    database_type: ''
+    database_type: '',
   });
   const { Text, Title } = Typography;
   const formRef = useRef(null);
-  
+
   const [formData, setFormData] = useState({
     username: '',
     password: '',
     confirmPassword: '',
-    usageMode: 'external'
+    usageMode: 'external',
   });
 
   useEffect(() => {
@@ -38,7 +54,7 @@ const Setup = () => {
       const { success, data } = res.data;
       if (success) {
         setSetupStatus(data);
-        
+
         // If setup is already completed, redirect to home
         if (data.status) {
           window.location.href = '/';
@@ -53,54 +69,54 @@ const Setup = () => {
   };
 
   const handleUsageModeChange = (val) => {
-    setFormData({...formData, usageMode: val});
+    setFormData({ ...formData, usageMode: val });
   };
 
   const onSubmit = () => {
     if (!formRef.current) {
-      console.error("Form reference is null");
+      console.error('Form reference is null');
       showError(t('表单引用错误,请刷新页面重试'));
       return;
     }
-    
+
     const values = formRef.current.getValues();
-    console.log("Form values:", values);
-    
+    console.log('Form values:', values);
+
     // For root_init=false, validate admin username and password
     if (!setupStatus.root_init) {
       if (!values.username || !values.username.trim()) {
         showError(t('请输入管理员用户名'));
         return;
       }
-      
+
       if (!values.password || values.password.length < 8) {
         showError(t('密码长度至少为8个字符'));
         return;
       }
-      
+
       if (values.password !== values.confirmPassword) {
         showError(t('两次输入的密码不一致'));
         return;
       }
     }
-    
+
     // Prepare submission data
-    const formValues = {...values};
+    const formValues = { ...values };
     formValues.SelfUseModeEnabled = values.usageMode === 'self';
     formValues.DemoSiteEnabled = values.usageMode === 'demo';
-    
+
     // Remove usageMode as it's not needed by the backend
     delete formValues.usageMode;
-    
-    console.log("Submitting data to backend:", formValues);
+
+    console.log('Submitting data to backend:', formValues);
     setLoading(true);
-    
+
     // Submit to backend
     API.post('/api/setup', formValues)
-      .then(res => {
+      .then((res) => {
         const { success, message } = res.data;
-        console.log("API response:", res.data);
-        
+        console.log('API response:', res.data);
+
         if (success) {
           showNotice(t('系统初始化成功,正在跳转...'));
           setTimeout(() => {
@@ -110,7 +126,7 @@ const Setup = () => {
           showError(message || t('初始化失败,请重试'));
         }
       })
-      .catch(error => {
+      .catch((error) => {
         console.error('API error:', error);
         showError(t('系统初始化失败,请重试'));
         setLoading(false);
@@ -124,31 +140,44 @@ const Setup = () => {
     <>
       <div style={{ maxWidth: '800px', margin: '0 auto', padding: '20px' }}>
         <Card>
-          <Title heading={2} style={{ marginBottom: '24px' }}>{t('系统初始化')}</Title>
-          
+          <Title heading={2} style={{ marginBottom: '24px' }}>
+            {t('系统初始化')}
+          </Title>
+
           {setupStatus.database_type === 'sqlite' && (
             <Banner
-              type="warning"
-              icon={<IconAlertTriangle size="large" />}
+              type='warning'
+              icon={<IconAlertTriangle size='large' />}
               closeIcon={null}
               title={t('数据库警告')}
               description={
                 <div>
-                  <p>{t('您正在使用 SQLite 数据库。如果您在容器环境中运行,请确保已正确设置数据库文件的持久化映射,否则容器重启后所有数据将丢失!')}</p>
-                  <p>{t('建议在生产环境中使用 MySQL 或 PostgreSQL 数据库,或确保 SQLite 数据库文件已映射到宿主机的持久化存储。')}</p>
+                  <p>
+                    {t(
+                      '您正在使用 SQLite 数据库。如果您在容器环境中运行,请确保已正确设置数据库文件的持久化映射,否则容器重启后所有数据将丢失!',
+                    )}
+                  </p>
+                  <p>
+                    {t(
+                      '建议在生产环境中使用 MySQL 或 PostgreSQL 数据库,或确保 SQLite 数据库文件已映射到宿主机的持久化存储。',
+                    )}
+                  </p>
                 </div>
               }
               style={{ marginBottom: '24px' }}
             />
           )}
-          
+
           <Form
-            getFormApi={(formApi) => { formRef.current = formApi; console.log("Form API set:", formApi); }}
+            getFormApi={(formApi) => {
+              formRef.current = formApi;
+              console.log('Form API set:', formApi);
+            }}
             initValues={formData}
           >
             {setupStatus.root_init ? (
               <Banner
-                type="info"
+                type='info'
                 icon={<IconInfoCircle />}
                 closeIcon={null}
                 description={t('管理员账号已经初始化过,请继续设置系统参数')}
@@ -157,43 +186,56 @@ const Setup = () => {
             ) : (
               <Form.Section text={t('管理员账号')}>
                 <Form.Input
-                  field="username"
+                  field='username'
                   label={t('用户名')}
                   placeholder={t('请输入管理员用户名')}
                   showClear
-                  onChange={(value) => setFormData({...formData, username: value})}
+                  onChange={(value) =>
+                    setFormData({ ...formData, username: value })
+                  }
                 />
                 <Form.Input
-                  field="password"
+                  field='password'
                   label={t('密码')}
                   placeholder={t('请输入管理员密码')}
-                  type="password"
+                  type='password'
                   showClear
-                  onChange={(value) => setFormData({...formData, password: value})}
+                  onChange={(value) =>
+                    setFormData({ ...formData, password: value })
+                  }
                 />
                 <Form.Input
-                  field="confirmPassword"
+                  field='confirmPassword'
                   label={t('确认密码')}
                   placeholder={t('请确认管理员密码')}
-                  type="password"
+                  type='password'
                   showClear
-                  onChange={(value) => setFormData({...formData, confirmPassword: value})}
+                  onChange={(value) =>
+                    setFormData({ ...formData, confirmPassword: value })
+                  }
                 />
               </Form.Section>
             )}
-            
-            <Form.Section text={
-              <div style={{ display: 'flex', alignItems: 'center' }}>
-                {t('系统设置')}
-              </div>
-            }>
-              <Form.RadioGroup 
-                field="usageMode" 
+
+            <Form.Section
+              text={
+                <div style={{ display: 'flex', alignItems: 'center' }}>
+                  {t('系统设置')}
+                </div>
+              }
+            >
+              <Form.RadioGroup
+                field='usageMode'
                 label={
                   <div style={{ display: 'flex', alignItems: 'center' }}>
                     {t('使用模式')}
-                    <IconHelpCircle 
-                      style={{ marginLeft: '4px', color: 'var(--semi-color-primary)', verticalAlign: 'middle', cursor: 'pointer' }} 
+                    <IconHelpCircle
+                      style={{
+                        marginLeft: '4px',
+                        color: 'var(--semi-color-primary)',
+                        verticalAlign: 'middle',
+                        cursor: 'pointer',
+                      }}
                       onClick={(e) => {
                         // e.preventDefault();
                         // e.stopPropagation();
@@ -203,18 +245,18 @@ const Setup = () => {
                   </div>
                 }
                 extraText={t('可在初始化后修改')}
-                initValue="external"
+                initValue='external'
                 onChange={handleUsageModeChange}
               >
-                <Form.Radio value="external">{t('对外运营模式')}</Form.Radio>
-                <Form.Radio value="self">{t('自用模式')}</Form.Radio>
-                <Form.Radio value="demo">{t('演示站点模式')}</Form.Radio>
+                <Form.Radio value='external'>{t('对外运营模式')}</Form.Radio>
+                <Form.Radio value='self'>{t('自用模式')}</Form.Radio>
+                <Form.Radio value='demo'>{t('演示站点模式')}</Form.Radio>
               </Form.RadioGroup>
             </Form.Section>
           </Form>
 
           <div style={{ marginTop: '24px', textAlign: 'right' }}>
-            <Button type="primary" onClick={onSubmit} loading={loading}>
+            <Button type='primary' onClick={onSubmit} loading={loading}>
               {t('初始化系统')}
             </Button>
           </div>
@@ -233,12 +275,18 @@ const Setup = () => {
         <div style={{ padding: '8px 0' }}>
           <Title heading={6}>{t('对外运营模式')}</Title>
           <p>{t('默认模式,适用于为多个用户提供服务的场景。')}</p>
-          <p>{t('此模式下,系统将计算每次调用的用量,您需要对每个模型都设置价格,如果没有设置价格,用户将无法使用该模型。')}</p>
+          <p>
+            {t(
+              '此模式下,系统将计算每次调用的用量,您需要对每个模型都设置价格,如果没有设置价格,用户将无法使用该模型。',
+            )}
+          </p>
         </div>
         <div style={{ padding: '8px 0' }}>
           <Title heading={6}>{t('自用模式')}</Title>
           <p>{t('适用于个人使用的场景。')}</p>
-          <p>{t('不需要设置模型价格,系统将弱化用量计算,您可专注于使用模型。')}</p>
+          <p>
+            {t('不需要设置模型价格,系统将弱化用量计算,您可专注于使用模型。')}
+          </p>
         </div>
         <div style={{ padding: '8px 0' }}>
           <Title heading={6}>{t('演示站点模式')}</Title>

+ 1 - 1
web/src/pages/Task/index.js

@@ -1,5 +1,5 @@
 import React from 'react';
-import TaskLogsTable from "../../components/TaskLogsTable.js";
+import TaskLogsTable from '../../components/TaskLogsTable.js';
 
 const Task = () => (
   <>

+ 19 - 13
web/src/pages/Token/EditToken.js

@@ -18,8 +18,9 @@ import {
   Select,
   SideSheet,
   Space,
-  Spin, TextArea,
-  Typography
+  Spin,
+  TextArea,
+  Typography,
 } from '@douyinfe/semi-ui';
 import Title from '@douyinfe/semi-ui/lib/es/typography/title';
 import { Divider } from 'semantic-ui-react';
@@ -47,7 +48,7 @@ const EditToken = (props) => {
     model_limits_enabled,
     model_limits,
     allow_ips,
-    group
+    group,
   } = inputs;
   // const [visible, setVisible] = useState(false);
   const [models, setModels] = useState([]);
@@ -100,7 +101,7 @@ const EditToken = (props) => {
       let localGroupOptions = Object.entries(data).map(([group, info]) => ({
         label: info.desc,
         value: group,
-        ratio: info.ratio
+        ratio: info.ratio,
       }));
       setGroups(localGroupOptions);
     } else {
@@ -229,9 +230,7 @@ const EditToken = (props) => {
       }
 
       if (successCount > 0) {
-        showSuccess(
-          t('令牌创建成功,请在列表页面点击复制获取令牌!')
-        );
+        showSuccess(t('令牌创建成功,请在列表页面点击复制获取令牌!'));
         props.refresh();
         props.handleClose();
       }
@@ -246,7 +245,9 @@ const EditToken = (props) => {
       <SideSheet
         placement={isEdit ? 'right' : 'left'}
         title={
-          <Title level={3}>{isEdit ? t('更新令牌信息') : t('创建新的令牌')}</Title>
+          <Title level={3}>
+            {isEdit ? t('更新令牌信息') : t('创建新的令牌')}
+          </Title>
         }
         headerStyle={{ borderBottom: '1px solid var(--semi-color-border)' }}
         bodyStyle={{ borderBottom: '1px solid var(--semi-color-border)' }}
@@ -333,7 +334,9 @@ const EditToken = (props) => {
           <Divider />
           <Banner
             type={'warning'}
-            description={t('注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。')}
+            description={t(
+              '注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。',
+            )}
           ></Banner>
           <div style={{ marginTop: 20 }}>
             <Typography.Text>{`${t('额度')}${renderQuotaWithPrompt(remain_quota)}`}</Typography.Text>
@@ -396,7 +399,9 @@ const EditToken = (props) => {
           </div>
           <Divider />
           <div style={{ marginTop: 10 }}>
-            <Typography.Text>{t('IP白名单(请勿过度信任此功能)')}</Typography.Text>
+            <Typography.Text>
+              {t('IP白名单(请勿过度信任此功能)')}
+            </Typography.Text>
           </div>
           <TextArea
             label={t('IP白名单')}
@@ -440,7 +445,7 @@ const EditToken = (props) => {
           <div style={{ marginTop: 10 }}>
             <Typography.Text>{t('令牌分组,默认为用户的分组')}</Typography.Text>
           </div>
-          {groups.length > 0 ?
+          {groups.length > 0 ? (
             <Select
               style={{ marginTop: 8 }}
               placeholder={t('令牌分组,默认为用户的分组')}
@@ -455,14 +460,15 @@ const EditToken = (props) => {
               value={inputs.group}
               autoComplete='new-password'
               optionList={groups}
-            />:
+            />
+          ) : (
             <Select
               style={{ marginTop: 8 }}
               placeholder={t('管理员未设置用户可选分组')}
               name='gruop'
               disabled={true}
             />
-          }
+          )}
         </Spin>
       </SideSheet>
     </>

+ 9 - 7
web/src/pages/Token/index.js

@@ -8,13 +8,15 @@ const Token = () => {
     <>
       <Layout>
         <Layout.Header>
-        <Banner
-          type='warning'
-          description={t('令牌无法精确控制使用额度,只允许自用,请勿直接将令牌分发给他人。')}
-        />
-      </Layout.Header>
-      <Layout.Content>
-        <TokensTable />
+          <Banner
+            type='warning'
+            description={t(
+              '令牌无法精确控制使用额度,只允许自用,请勿直接将令牌分发给他人。',
+            )}
+          />
+        </Layout.Header>
+        <Layout.Content>
+          <TokensTable />
         </Layout.Content>
       </Layout>
     </>

+ 9 - 3
web/src/pages/TopUp/index.js

@@ -228,8 +228,12 @@ const TopUp = () => {
             size={'small'}
             centered={true}
           >
-            <p>{t('充值数量')}:{topUpCount}</p>
-            <p>{t('实付金额')}:{renderAmount()}</p>
+            <p>
+              {t('充值数量')}:{topUpCount}
+            </p>
+            <p>
+              {t('实付金额')}:{renderAmount()}
+            </p>
             <p>{t('是否确认充值?')}</p>
           </Modal>
           <div
@@ -280,7 +284,9 @@ const TopUp = () => {
                     disabled={!enableOnlineTopUp}
                     field={'redemptionCount'}
                     label={t('实付金额:') + ' ' + renderAmount()}
-                    placeholder={t('充值数量,最低 ') + renderQuotaWithAmount(minTopUp)}
+                    placeholder={
+                      t('充值数量,最低 ') + renderQuotaWithAmount(minTopUp)
+                    }
                     name='redemptionCount'
                     type={'number'}
                     value={topUpCount}

+ 21 - 9
web/src/pages/User/EditUser.js

@@ -201,7 +201,9 @@ const EditUser = (props) => {
                 search
                 selection
                 allowAdditions
-                additionLabel={t('请在系统设置页面编辑分组倍率以添加新的分组:')}
+                additionLabel={t(
+                  '请在系统设置页面编辑分组倍率以添加新的分组:',
+                )}
                 onChange={(value) => handleInputChange('group', value)}
                 value={inputs.group}
                 autoComplete='new-password'
@@ -231,17 +233,21 @@ const EditUser = (props) => {
             name='github_id'
             value={github_id}
             autoComplete='new-password'
-            placeholder={t('此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改')}
+            placeholder={t(
+              '此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改',
+            )}
             readonly
           />
           <div style={{ marginTop: 20 }}>
             <Typography.Text>{t('`已绑定的 OIDC 账户')}</Typography.Text>
           </div>
           <Input
-              name='oidc_id'
-              value={oidc_id}
-              placeholder={t('此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改')}
-              readonly
+            name='oidc_id'
+            value={oidc_id}
+            placeholder={t(
+              '此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改',
+            )}
+            readonly
           />
           <div style={{ marginTop: 20 }}>
             <Typography.Text>{t('已绑定的微信账户')}</Typography.Text>
@@ -250,7 +256,9 @@ const EditUser = (props) => {
             name='wechat_id'
             value={wechat_id}
             autoComplete='new-password'
-            placeholder={t('此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改')}
+            placeholder={t(
+              '此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改',
+            )}
             readonly
           />
           <div style={{ marginTop: 20 }}>
@@ -260,7 +268,9 @@ const EditUser = (props) => {
             name='email'
             value={email}
             autoComplete='new-password'
-            placeholder={t('此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改')}
+            placeholder={t(
+              '此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改',
+            )}
             readonly
           />
           <div style={{ marginTop: 20 }}>
@@ -270,7 +280,9 @@ const EditUser = (props) => {
             name='telegram_id'
             value={telegram_id}
             autoComplete='new-password'
-            placeholder={t('此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改')}
+            placeholder={t(
+              '此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改',
+            )}
             readonly
           />
         </Spin>

+ 4 - 4
web/src/pages/User/index.js

@@ -9,10 +9,10 @@ const User = () => {
     <>
       <Layout>
         <Layout.Header>
-        <h3>{t('管理用户')}</h3>
-      </Layout.Header>
-      <Layout.Content>
-        <UsersTable />
+          <h3>{t('管理用户')}</h3>
+        </Layout.Header>
+        <Layout.Content>
+          <UsersTable />
         </Layout.Content>
       </Layout>
     </>

+ 5 - 1
web/vite.config.js

@@ -46,7 +46,11 @@ export default defineConfig({
             'react-toastify',
             'react-turnstile',
           ],
-          'i18n': ['i18next', 'react-i18next', 'i18next-browser-languagedetector'],
+          i18n: [
+            'i18next',
+            'react-i18next',
+            'i18next-browser-languagedetector',
+          ],
         },
       },
     },

Некоторые файлы не были показаны из-за большого количества измененных файлов