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

✨ feat(ratio-sync, ui): add built‑in “Official Ratio Preset” and harden upstream sync

Backend (controller/ratio_sync.go):
- Add built‑in official upstream to GetSyncableChannels (ID: -100, BaseURL: https://basellm.github.io)
- Support absolute endpoint URLs; otherwise join BaseURL + endpoint (defaults to /api/ratio_config)
- Harden HTTP client:
  - IPv4‑first with IPv6 fallback for github.io
  - Add ResponseHeaderTimeout
  - 3 attempts with exponential backoff (200/400/800ms)
- Validate Content-Type and limit response body to 10MB (safe decode via io.LimitReader)
- Robust parsing: support type1 ratio_config map and type2 pricing list
- Use net.SplitHostPort for host parsing
- Use float tolerance in differences comparison to avoid false mismatches
- Remove unused code (tryDirect) and improve warnings

Frontend:
- UpstreamRatioSync.jsx: auto-assign official endpoint to /llm-metadata/api/newapi/ratio_config-v1-base.json
- ChannelSelectorModal.jsx:
  - Pin the official source at the top of the list
  - Show a green “官方” tag next to the status
  - Refactor status renderer to accept the full record

Notes:
- Backward compatible; no API surface changes
- Official ratio_config reference: https://basellm.github.io/llm-metadata/api/newapi/ratio_config-v1-base.json
t0ng7u 6 месяцев назад
Родитель
Сommit
1f111a163a

+ 78 - 13
controller/ratio_sync.go

@@ -4,6 +4,8 @@ import (
 	"context"
 	"encoding/json"
 	"fmt"
+	"io"
+	"net"
 	"net/http"
 	"one-api/logger"
 	"strings"
@@ -21,8 +23,26 @@ const (
 	defaultTimeoutSeconds = 10
 	defaultEndpoint       = "/api/ratio_config"
 	maxConcurrentFetches  = 8
+	maxRatioConfigBytes   = 10 << 20 // 10MB
+	floatEpsilon          = 1e-9
 )
 
+func nearlyEqual(a, b float64) bool {
+	if a > b {
+		return a-b < floatEpsilon
+	}
+	return b-a < floatEpsilon
+}
+
+func valuesEqual(a, b interface{}) bool {
+	af, aok := a.(float64)
+	bf, bok := b.(float64)
+	if aok && bok {
+		return nearlyEqual(af, bf)
+	}
+	return a == b
+}
+
 var ratioTypes = []string{"model_ratio", "completion_ratio", "cache_ratio", "model_price"}
 
 type upstreamResult struct {
@@ -87,7 +107,23 @@ func FetchUpstreamRatios(c *gin.Context) {
 
 	sem := make(chan struct{}, maxConcurrentFetches)
 
-	client := &http.Client{Transport: &http.Transport{MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second}}
+	dialer := &net.Dialer{Timeout: 10 * time.Second}
+	transport := &http.Transport{MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, ResponseHeaderTimeout: 10 * time.Second}
+	transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
+		host, _, err := net.SplitHostPort(addr)
+		if err != nil {
+			host = addr
+		}
+		// 对 github.io 优先尝试 IPv4,失败则回退 IPv6
+		if strings.HasSuffix(host, "github.io") {
+			if conn, err := dialer.DialContext(ctx, "tcp4", addr); err == nil {
+				return conn, nil
+			}
+			return dialer.DialContext(ctx, "tcp6", addr)
+		}
+		return dialer.DialContext(ctx, network, addr)
+	}
+	client := &http.Client{Transport: transport}
 
 	for _, chn := range upstreams {
 		wg.Add(1)
@@ -98,12 +134,17 @@ func FetchUpstreamRatios(c *gin.Context) {
 			defer func() { <-sem }()
 
 			endpoint := chItem.Endpoint
-			if endpoint == "" {
-				endpoint = defaultEndpoint
-			} else if !strings.HasPrefix(endpoint, "/") {
-				endpoint = "/" + endpoint
+			var fullURL string
+			if strings.HasPrefix(endpoint, "http://") || strings.HasPrefix(endpoint, "https://") {
+				fullURL = endpoint
+			} else {
+				if endpoint == "" {
+					endpoint = defaultEndpoint
+				} else if !strings.HasPrefix(endpoint, "/") {
+					endpoint = "/" + endpoint
+				}
+				fullURL = chItem.BaseURL + endpoint
 			}
-			fullURL := chItem.BaseURL + endpoint
 
 			uniqueName := chItem.Name
 			if chItem.ID != 0 {
@@ -120,10 +161,19 @@ func FetchUpstreamRatios(c *gin.Context) {
 				return
 			}
 
-			resp, err := client.Do(httpReq)
-			if err != nil {
-				logger.LogWarn(c.Request.Context(), "http error on "+chItem.Name+": "+err.Error())
-				ch <- upstreamResult{Name: uniqueName, Err: err.Error()}
+			// 简单重试:最多 3 次,指数退避
+			var resp *http.Response
+			var lastErr error
+			for attempt := 0; attempt < 3; attempt++ {
+				resp, lastErr = client.Do(httpReq)
+				if lastErr == nil {
+					break
+				}
+				time.Sleep(time.Duration(200*(1<<attempt)) * time.Millisecond)
+			}
+			if lastErr != nil {
+				logger.LogWarn(c.Request.Context(), "http error on "+chItem.Name+": "+lastErr.Error())
+				ch <- upstreamResult{Name: uniqueName, Err: lastErr.Error()}
 				return
 			}
 			defer resp.Body.Close()
@@ -132,6 +182,12 @@ func FetchUpstreamRatios(c *gin.Context) {
 				ch <- upstreamResult{Name: uniqueName, Err: resp.Status}
 				return
 			}
+
+			// Content-Type 和响应体大小校验
+			if ct := resp.Header.Get("Content-Type"); ct != "" && !strings.Contains(strings.ToLower(ct), "application/json") {
+				logger.LogWarn(c.Request.Context(), "unexpected content-type from "+chItem.Name+": "+ct)
+			}
+			limited := io.LimitReader(resp.Body, maxRatioConfigBytes)
 			// 兼容两种上游接口格式:
 			//  type1: /api/ratio_config -> data 为 map[string]any,包含 model_ratio/completion_ratio/cache_ratio/model_price
 			//  type2: /api/pricing      -> data 为 []Pricing 列表,需要转换为与 type1 相同的 map 格式
@@ -141,7 +197,7 @@ func FetchUpstreamRatios(c *gin.Context) {
 				Message string          `json:"message"`
 			}
 
-			if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
+			if err := json.NewDecoder(limited).Decode(&body); err != nil {
 				logger.LogWarn(c.Request.Context(), "json decode failed from "+chItem.Name+": "+err.Error())
 				ch <- upstreamResult{Name: uniqueName, Err: err.Error()}
 				return
@@ -152,6 +208,8 @@ func FetchUpstreamRatios(c *gin.Context) {
 				return
 			}
 
+			// 若 Data 为空,将继续按 type1 尝试解析(与多数静态 ratio_config 兼容)
+
 			// 尝试按 type1 解析
 			var type1Data map[string]any
 			if err := json.Unmarshal(body.Data, &type1Data); err == nil {
@@ -357,9 +415,9 @@ func buildDifferences(localData map[string]any, successfulChannels []struct {
 						upstreamValue = val
 						hasUpstreamValue = true
 
-						if localValue != nil && localValue != val {
+						if localValue != nil && !valuesEqual(localValue, val) {
 							hasDifference = true
-						} else if localValue == val {
+						} else if valuesEqual(localValue, val) {
 							upstreamValue = "same"
 						}
 					}
@@ -466,6 +524,13 @@ func GetSyncableChannels(c *gin.Context) {
 		}
 	}
 
+	syncableChannels = append(syncableChannels, dto.SyncableChannel{
+		ID:      -100,
+		Name:    "官方倍率预设",
+		BaseURL: "https://basellm.github.io",
+		Status:  1,
+	})
+
 	c.JSON(http.StatusOK, gin.H{
 		"success": true,
 		"message": "",

+ 4 - 1
web/src/App.jsx

@@ -279,7 +279,10 @@ function App() {
           element={
             pricingRequireAuth ? (
               <PrivateRoute>
-                <Suspense fallback={<Loading></Loading>} key={location.pathname}>
+                <Suspense
+                  fallback={<Loading></Loading>}
+                  key={location.pathname}
+                >
                   <Pricing />
                 </Suspense>
               </PrivateRoute>

+ 7 - 1
web/src/components/layout/HeaderBar/Navigation.jsx

@@ -21,7 +21,13 @@ import React from 'react';
 import { Link } from 'react-router-dom';
 import SkeletonWrapper from './SkeletonWrapper';
 
-const Navigation = ({ mainNavLinks, isMobile, isLoading, userState, pricingRequireAuth }) => {
+const Navigation = ({
+  mainNavLinks,
+  isMobile,
+  isLoading,
+  userState,
+  pricingRequireAuth,
+}) => {
   const renderNavLinks = () => {
     const baseClasses =
       'flex-shrink-0 flex items-center gap-1 font-semibold rounded-md transition-all duration-200 ease-in-out';

+ 150 - 156
web/src/components/layout/SiderBar.jsx

@@ -50,7 +50,11 @@ const routerMap = {
 const SiderBar = ({ onNavigate = () => {} }) => {
   const { t } = useTranslation();
   const [collapsed, toggleCollapsed] = useSidebarCollapsed();
-  const { isModuleVisible, hasSectionVisibleModules, loading: sidebarLoading } = useSidebar();
+  const {
+    isModuleVisible,
+    hasSectionVisibleModules,
+    loading: sidebarLoading,
+  } = useSidebar();
 
   const [selectedKeys, setSelectedKeys] = useState(['home']);
   const [chatItems, setChatItems] = useState([]);
@@ -58,160 +62,148 @@ const SiderBar = ({ onNavigate = () => {} }) => {
   const location = useLocation();
   const [routerMapState, setRouterMapState] = useState(routerMap);
 
-  const workspaceItems = useMemo(
-    () => {
-      const items = [
-        {
-          text: t('数据看板'),
-          itemKey: 'detail',
-          to: '/detail',
-          className:
-            localStorage.getItem('enable_data_export') === 'true'
-              ? ''
-              : 'tableHiddle',
-        },
-        {
-          text: t('令牌管理'),
-          itemKey: 'token',
-          to: '/token',
-        },
-        {
-          text: t('使用日志'),
-          itemKey: 'log',
-          to: '/log',
-        },
-        {
-          text: t('绘图日志'),
-          itemKey: 'midjourney',
-          to: '/midjourney',
-          className:
-            localStorage.getItem('enable_drawing') === 'true'
-              ? ''
-              : 'tableHiddle',
-        },
-        {
-          text: t('任务日志'),
-          itemKey: 'task',
-          to: '/task',
-          className:
-            localStorage.getItem('enable_task') === 'true' ? '' : 'tableHiddle',
-        },
-      ];
-
-      // 根据配置过滤项目
-      const filteredItems = items.filter(item => {
-        const configVisible = isModuleVisible('console', item.itemKey);
-        return configVisible;
-      });
-
-      return filteredItems;
-    },
-    [
-      localStorage.getItem('enable_data_export'),
-      localStorage.getItem('enable_drawing'),
-      localStorage.getItem('enable_task'),
-      t,
-      isModuleVisible,
-    ],
-  );
-
-  const financeItems = useMemo(
-    () => {
-      const items = [
-        {
-          text: t('钱包管理'),
-          itemKey: 'topup',
-          to: '/topup',
-        },
-        {
-          text: t('个人设置'),
-          itemKey: 'personal',
-          to: '/personal',
-        },
-      ];
-
-      // 根据配置过滤项目
-      const filteredItems = items.filter(item => {
-        const configVisible = isModuleVisible('personal', item.itemKey);
-        return configVisible;
-      });
-
-      return filteredItems;
-    },
-    [t, isModuleVisible],
-  );
-
-  const adminItems = useMemo(
-    () => {
-      const items = [
-        {
-          text: t('渠道管理'),
-          itemKey: 'channel',
-          to: '/channel',
-          className: isAdmin() ? '' : 'tableHiddle',
-        },
-        {
-          text: t('模型管理'),
-          itemKey: 'models',
-          to: '/console/models',
-          className: isAdmin() ? '' : 'tableHiddle',
-        },
-        {
-          text: t('兑换码管理'),
-          itemKey: 'redemption',
-          to: '/redemption',
-          className: isAdmin() ? '' : 'tableHiddle',
-        },
-        {
-          text: t('用户管理'),
-          itemKey: 'user',
-          to: '/user',
-          className: isAdmin() ? '' : 'tableHiddle',
-        },
-        {
-          text: t('系统设置'),
-          itemKey: 'setting',
-          to: '/setting',
-          className: isRoot() ? '' : 'tableHiddle',
-        },
-      ];
-
-      // 根据配置过滤项目
-      const filteredItems = items.filter(item => {
-        const configVisible = isModuleVisible('admin', item.itemKey);
-        return configVisible;
-      });
-
-      return filteredItems;
-    },
-    [isAdmin(), isRoot(), t, isModuleVisible],
-  );
-
-  const chatMenuItems = useMemo(
-    () => {
-      const items = [
-        {
-          text: t('操练场'),
-          itemKey: 'playground',
-          to: '/playground',
-        },
-        {
-          text: t('聊天'),
-          itemKey: 'chat',
-          items: chatItems,
-        },
-      ];
-
-      // 根据配置过滤项目
-      const filteredItems = items.filter(item => {
-        const configVisible = isModuleVisible('chat', item.itemKey);
-        return configVisible;
-      });
-
-      return filteredItems;
-    },
-    [chatItems, t, isModuleVisible],
-  );
+  const workspaceItems = useMemo(() => {
+    const items = [
+      {
+        text: t('数据看板'),
+        itemKey: 'detail',
+        to: '/detail',
+        className:
+          localStorage.getItem('enable_data_export') === 'true'
+            ? ''
+            : 'tableHiddle',
+      },
+      {
+        text: t('令牌管理'),
+        itemKey: 'token',
+        to: '/token',
+      },
+      {
+        text: t('使用日志'),
+        itemKey: 'log',
+        to: '/log',
+      },
+      {
+        text: t('绘图日志'),
+        itemKey: 'midjourney',
+        to: '/midjourney',
+        className:
+          localStorage.getItem('enable_drawing') === 'true'
+            ? ''
+            : 'tableHiddle',
+      },
+      {
+        text: t('任务日志'),
+        itemKey: 'task',
+        to: '/task',
+        className:
+          localStorage.getItem('enable_task') === 'true' ? '' : 'tableHiddle',
+      },
+    ];
+
+    // 根据配置过滤项目
+    const filteredItems = items.filter((item) => {
+      const configVisible = isModuleVisible('console', item.itemKey);
+      return configVisible;
+    });
+
+    return filteredItems;
+  }, [
+    localStorage.getItem('enable_data_export'),
+    localStorage.getItem('enable_drawing'),
+    localStorage.getItem('enable_task'),
+    t,
+    isModuleVisible,
+  ]);
+
+  const financeItems = useMemo(() => {
+    const items = [
+      {
+        text: t('钱包管理'),
+        itemKey: 'topup',
+        to: '/topup',
+      },
+      {
+        text: t('个人设置'),
+        itemKey: 'personal',
+        to: '/personal',
+      },
+    ];
+
+    // 根据配置过滤项目
+    const filteredItems = items.filter((item) => {
+      const configVisible = isModuleVisible('personal', item.itemKey);
+      return configVisible;
+    });
+
+    return filteredItems;
+  }, [t, isModuleVisible]);
+
+  const adminItems = useMemo(() => {
+    const items = [
+      {
+        text: t('渠道管理'),
+        itemKey: 'channel',
+        to: '/channel',
+        className: isAdmin() ? '' : 'tableHiddle',
+      },
+      {
+        text: t('模型管理'),
+        itemKey: 'models',
+        to: '/console/models',
+        className: isAdmin() ? '' : 'tableHiddle',
+      },
+      {
+        text: t('兑换码管理'),
+        itemKey: 'redemption',
+        to: '/redemption',
+        className: isAdmin() ? '' : 'tableHiddle',
+      },
+      {
+        text: t('用户管理'),
+        itemKey: 'user',
+        to: '/user',
+        className: isAdmin() ? '' : 'tableHiddle',
+      },
+      {
+        text: t('系统设置'),
+        itemKey: 'setting',
+        to: '/setting',
+        className: isRoot() ? '' : 'tableHiddle',
+      },
+    ];
+
+    // 根据配置过滤项目
+    const filteredItems = items.filter((item) => {
+      const configVisible = isModuleVisible('admin', item.itemKey);
+      return configVisible;
+    });
+
+    return filteredItems;
+  }, [isAdmin(), isRoot(), t, isModuleVisible]);
+
+  const chatMenuItems = useMemo(() => {
+    const items = [
+      {
+        text: t('操练场'),
+        itemKey: 'playground',
+        to: '/playground',
+      },
+      {
+        text: t('聊天'),
+        itemKey: 'chat',
+        items: chatItems,
+      },
+    ];
+
+    // 根据配置过滤项目
+    const filteredItems = items.filter((item) => {
+      const configVisible = isModuleVisible('chat', item.itemKey);
+      return configVisible;
+    });
+
+    return filteredItems;
+  }, [chatItems, t, isModuleVisible]);
 
   // 更新路由映射,添加聊天路由
   const updateRouterMapWithChats = (chats) => {
@@ -426,7 +418,9 @@ const SiderBar = ({ onNavigate = () => {} }) => {
         {/* 聊天区域 */}
         {hasSectionVisibleModules('chat') && (
           <div className='sidebar-section'>
-            {!collapsed && <div className='sidebar-group-label'>{t('聊天')}</div>}
+            {!collapsed && (
+              <div className='sidebar-group-label'>{t('聊天')}</div>
+            )}
             {chatMenuItems.map((item) => renderSubItem(item))}
           </div>
         )}

+ 45 - 25
web/src/components/settings/ChannelSelectorModal.jsx

@@ -34,7 +34,6 @@ import {
   Tag,
 } from '@douyinfe/semi-ui';
 import { IconSearch } from '@douyinfe/semi-icons';
-import { CheckCircle, XCircle, AlertCircle, HelpCircle } from 'lucide-react';
 
 const ChannelSelectorModal = forwardRef(
   (
@@ -65,6 +64,18 @@ const ChannelSelectorModal = forwardRef(
       },
     }));
 
+    // 官方渠道识别
+    const isOfficialChannel = (record) => {
+      const id = record?.key ?? record?.value ?? record?._originalData?.id;
+      const base = record?._originalData?.base_url || '';
+      const name = record?.label || '';
+      return (
+        id === -100 ||
+        base === 'https://basellm.github.io' ||
+        name === '官方倍率预设'
+      );
+    };
+
     useEffect(() => {
       if (!allChannels) return;
 
@@ -77,7 +88,13 @@ const ChannelSelectorModal = forwardRef(
           })
         : allChannels;
 
-      setFilteredData(matched);
+      const sorted = [...matched].sort((a, b) => {
+        const wa = isOfficialChannel(a) ? 0 : 1;
+        const wb = isOfficialChannel(b) ? 0 : 1;
+        return wa - wb;
+      });
+
+      setFilteredData(sorted);
     }, [allChannels, searchText]);
 
     const total = filteredData.length;
@@ -143,45 +160,49 @@ const ChannelSelectorModal = forwardRef(
       );
     };
 
-    const renderStatusCell = (status) => {
+    const renderStatusCell = (record) => {
+      const status = record?._originalData?.status || 0;
+      const official = isOfficialChannel(record);
+      let statusTag = null;
       switch (status) {
         case 1:
-          return (
-            <Tag
-              color='green'
-              shape='circle'
-              prefixIcon={<CheckCircle size={14} />}
-            >
+          statusTag = (
+            <Tag color='green' shape='circle'>
               {t('已启用')}
             </Tag>
           );
+          break;
         case 2:
-          return (
-            <Tag color='red' shape='circle' prefixIcon={<XCircle size={14} />}>
+          statusTag = (
+            <Tag color='red' shape='circle'>
               {t('已禁用')}
             </Tag>
           );
+          break;
         case 3:
-          return (
-            <Tag
-              color='yellow'
-              shape='circle'
-              prefixIcon={<AlertCircle size={14} />}
-            >
+          statusTag = (
+            <Tag color='yellow' shape='circle'>
               {t('自动禁用')}
             </Tag>
           );
+          break;
         default:
-          return (
-            <Tag
-              color='grey'
-              shape='circle'
-              prefixIcon={<HelpCircle size={14} />}
-            >
+          statusTag = (
+            <Tag color='grey' shape='circle'>
               {t('未知状态')}
             </Tag>
           );
       }
+      return (
+        <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
+          {statusTag}
+          {official && (
+            <Tag color='green' shape='circle' type='light'>
+              {t('官方')}
+            </Tag>
+          )}
+        </div>
+      );
     };
 
     const renderNameCell = (text) => (
@@ -207,8 +228,7 @@ const ChannelSelectorModal = forwardRef(
       {
         title: t('状态'),
         dataIndex: '_originalData.status',
-        render: (_, record) =>
-          renderStatusCell(record._originalData?.status || 0),
+        render: (_, record) => renderStatusCell(record),
       },
       {
         title: t('同步接口'),

+ 0 - 4
web/src/components/settings/PersonalSetting.jsx

@@ -38,8 +38,6 @@ const PersonalSetting = () => {
   let navigate = useNavigate();
   const { t } = useTranslation();
 
-
-
   const [inputs, setInputs] = useState({
     wechat_verification_code: '',
     email_verification_code: '',
@@ -335,8 +333,6 @@ const PersonalSetting = () => {
               saveNotificationSettings={saveNotificationSettings}
             />
           </div>
-
-
         </div>
       </div>
 

+ 193 - 120
web/src/components/settings/personal/cards/NotificationSettings.jsx

@@ -34,7 +34,12 @@ import {
 } from '@douyinfe/semi-ui';
 import { IconMail, IconKey, IconBell, IconLink } from '@douyinfe/semi-icons';
 import { ShieldCheck, Bell, DollarSign, Settings } from 'lucide-react';
-import { renderQuotaWithPrompt, API, showSuccess, showError } from '../../../../helpers';
+import {
+  renderQuotaWithPrompt,
+  API,
+  showSuccess,
+  showError,
+} from '../../../../helpers';
 import CodeViewer from '../../../playground/CodeViewer';
 import { StatusContext } from '../../../../context/Status';
 import { UserContext } from '../../../../context/User';
@@ -57,7 +62,7 @@ const NotificationSettings = ({
     chat: {
       enabled: true,
       playground: true,
-      chat: true
+      chat: true,
     },
     console: {
       enabled: true,
@@ -65,12 +70,12 @@ const NotificationSettings = ({
       token: true,
       log: true,
       midjourney: true,
-      task: true
+      task: true,
     },
     personal: {
       enabled: true,
       topup: true,
-      personal: true
+      personal: true,
     },
     admin: {
       enabled: true,
@@ -78,8 +83,8 @@ const NotificationSettings = ({
       models: true,
       redemption: true,
       user: true,
-      setting: true
-    }
+      setting: true,
+    },
   });
   const [adminConfig, setAdminConfig] = useState(null);
 
@@ -99,8 +104,8 @@ const NotificationSettings = ({
         ...sidebarModulesUser,
         [sectionKey]: {
           ...sidebarModulesUser[sectionKey],
-          enabled: checked
-        }
+          enabled: checked,
+        },
       };
       setSidebarModulesUser(newModules);
     };
@@ -112,8 +117,8 @@ const NotificationSettings = ({
         ...sidebarModulesUser,
         [sectionKey]: {
           ...sidebarModulesUser[sectionKey],
-          [moduleKey]: checked
-        }
+          [moduleKey]: checked,
+        },
       };
       setSidebarModulesUser(newModules);
     };
@@ -123,7 +128,7 @@ const NotificationSettings = ({
     setSidebarLoading(true);
     try {
       const res = await API.put('/api/user/self', {
-        sidebar_modules: JSON.stringify(sidebarModulesUser)
+        sidebar_modules: JSON.stringify(sidebarModulesUser),
       });
       if (res.data.success) {
         showSuccess(t('侧边栏设置保存成功'));
@@ -139,9 +144,23 @@ const NotificationSettings = ({
   const resetSidebarModules = () => {
     const defaultConfig = {
       chat: { enabled: true, playground: true, chat: true },
-      console: { enabled: true, detail: true, token: true, log: true, midjourney: true, task: true },
+      console: {
+        enabled: true,
+        detail: true,
+        token: true,
+        log: true,
+        midjourney: true,
+        task: true,
+      },
       personal: { enabled: true, topup: true, personal: true },
-      admin: { enabled: true, channel: true, models: true, redemption: true, user: true, setting: true }
+      admin: {
+        enabled: true,
+        channel: true,
+        models: true,
+        redemption: true,
+        user: true,
+        setting: true,
+      },
     };
     setSidebarModulesUser(defaultConfig);
   };
@@ -187,7 +206,9 @@ const NotificationSettings = ({
     if (!adminConfig) return true;
 
     if (moduleKey) {
-      return adminConfig[sectionKey]?.enabled && adminConfig[sectionKey]?.[moduleKey];
+      return (
+        adminConfig[sectionKey]?.enabled && adminConfig[sectionKey]?.[moduleKey]
+      );
     } else {
       return adminConfig[sectionKey]?.enabled;
     }
@@ -200,9 +221,13 @@ const NotificationSettings = ({
       title: t('聊天区域'),
       description: t('操练场和聊天功能'),
       modules: [
-        { key: 'playground', title: t('操练场'), description: t('AI模型测试环境') },
-        { key: 'chat', title: t('聊天'), description: t('聊天会话管理') }
-      ]
+        {
+          key: 'playground',
+          title: t('操练场'),
+          description: t('AI模型测试环境'),
+        },
+        { key: 'chat', title: t('聊天'), description: t('聊天会话管理') },
+      ],
     },
     {
       key: 'console',
@@ -212,9 +237,13 @@ const NotificationSettings = ({
         { key: 'detail', title: t('数据看板'), description: t('系统数据统计') },
         { key: 'token', title: t('令牌管理'), description: t('API令牌管理') },
         { key: 'log', title: t('使用日志'), description: t('API使用记录') },
-        { key: 'midjourney', title: t('绘图日志'), description: t('绘图任务记录') },
-        { key: 'task', title: t('任务日志'), description: t('系统任务记录') }
-      ]
+        {
+          key: 'midjourney',
+          title: t('绘图日志'),
+          description: t('绘图任务记录'),
+        },
+        { key: 'task', title: t('任务日志'), description: t('系统任务记录') },
+      ],
     },
     {
       key: 'personal',
@@ -222,8 +251,12 @@ const NotificationSettings = ({
       description: t('用户个人功能'),
       modules: [
         { key: 'topup', title: t('钱包管理'), description: t('余额充值管理') },
-        { key: 'personal', title: t('个人设置'), description: t('个人信息设置') }
-      ]
+        {
+          key: 'personal',
+          title: t('个人设置'),
+          description: t('个人信息设置'),
+        },
+      ],
     },
     // 管理员区域:根据后端权限控制显示
     {
@@ -233,23 +266,35 @@ const NotificationSettings = ({
       modules: [
         { key: 'channel', title: t('渠道管理'), description: t('API渠道配置') },
         { key: 'models', title: t('模型管理'), description: t('AI模型配置') },
-        { key: 'redemption', title: t('兑换码管理'), description: t('兑换码生成管理') },
+        {
+          key: 'redemption',
+          title: t('兑换码管理'),
+          description: t('兑换码生成管理'),
+        },
         { key: 'user', title: t('用户管理'), description: t('用户账户管理') },
-        { key: 'setting', title: t('系统设置'), description: t('系统参数配置') }
-      ]
-    }
-  ].filter(section => {
-    // 使用后端权限验证替代前端角色判断
-    return isSidebarSectionAllowed(section.key);
-  }).map(section => ({
-    ...section,
-    modules: section.modules.filter(module =>
-      isSidebarModuleAllowed(section.key, module.key)
-    )
-  })).filter(section =>
-    // 过滤掉没有可用模块的区域
-    section.modules.length > 0 && isAllowedByAdmin(section.key)
-  );
+        {
+          key: 'setting',
+          title: t('系统设置'),
+          description: t('系统参数配置'),
+        },
+      ],
+    },
+  ]
+    .filter((section) => {
+      // 使用后端权限验证替代前端角色判断
+      return isSidebarSectionAllowed(section.key);
+    })
+    .map((section) => ({
+      ...section,
+      modules: section.modules.filter((module) =>
+        isSidebarModuleAllowed(section.key, module.key),
+      ),
+    }))
+    .filter(
+      (section) =>
+        // 过滤掉没有可用模块的区域
+        section.modules.length > 0 && isAllowedByAdmin(section.key),
+    );
 
   // 表单提交
   const handleSubmit = () => {
@@ -491,7 +536,9 @@ const NotificationSettings = ({
                     <Form.Input
                       field='barkUrl'
                       label={t('Bark推送URL')}
-                      placeholder={t('请输入Bark推送URL,例如: https://api.day.app/yourkey/{{title}}/{{content}}')}
+                      placeholder={t(
+                        '请输入Bark推送URL,例如: https://api.day.app/yourkey/{{title}}/{{content}}',
+                      )}
                       onChange={(val) => handleFormChange('barkUrl', val)}
                       prefix={<IconLink />}
                       extraText={t(
@@ -500,8 +547,7 @@ const NotificationSettings = ({
                       showClear
                       rules={[
                         {
-                          required:
-                            notificationSettings.warningType === 'bark',
+                          required: notificationSettings.warningType === 'bark',
                           message: t('请输入Bark推送URL'),
                         },
                         {
@@ -516,16 +562,23 @@ const NotificationSettings = ({
                         <strong>{t('模板示例')}</strong>
                       </div>
                       <div className='text-xs text-gray-600 font-mono bg-white p-3 rounded-lg shadow-sm mb-4'>
-                        https://api.day.app/yourkey/{'{{title}}'}/{'{{content}}'}?sound=alarm&group=quota
+                        https://api.day.app/yourkey/{'{{title}}'}/
+                        {'{{content}}'}?sound=alarm&group=quota
                       </div>
                       <div className='text-xs text-gray-500 space-y-2'>
-                        <div>• <strong>{'title'}:</strong> {t('通知标题')}</div>
-                        <div>• <strong>{'content'}:</strong> {t('通知内容')}</div>
+                        <div>
+                          • <strong>{'title'}:</strong> {t('通知标题')}
+                        </div>
+                        <div>
+                          • <strong>{'content'}:</strong> {t('通知内容')}
+                        </div>
                         <div className='mt-3 pt-3 border-t border-gray-200'>
-                          <span className='text-gray-400'>{t('更多参数请参考')}</span>{' '}
-                          <a 
-                            href='https://github.com/Finb/Bark' 
-                            target='_blank' 
+                          <span className='text-gray-400'>
+                            {t('更多参数请参考')}
+                          </span>{' '}
+                          <a
+                            href='https://github.com/Finb/Bark'
+                            target='_blank'
                             rel='noopener noreferrer'
                             className='text-blue-500 hover:text-blue-600 font-medium'
                           >
@@ -603,27 +656,25 @@ const NotificationSettings = ({
                 <div className='py-4'>
                   <div className='mb-4'>
                     <Typography.Text
-                      type="secondary"
-                      size="small"
+                      type='secondary'
+                      size='small'
                       style={{
                         fontSize: '12px',
                         lineHeight: '1.5',
-                        color: 'var(--semi-color-text-2)'
+                        color: 'var(--semi-color-text-2)',
                       }}
                     >
                       {t('您可以个性化设置侧边栏的要显示功能')}
                     </Typography.Text>
                   </div>
-
                   {/* 边栏设置功能区域容器 */}
                   <div
                     className='border rounded-xl p-4'
                     style={{
                       borderColor: 'var(--semi-color-border)',
-                      backgroundColor: 'var(--semi-color-bg-1)'
+                      backgroundColor: 'var(--semi-color-bg-1)',
                     }}
                   >
-
                     {sectionConfigs.map((section) => (
                       <div key={section.key} className='mb-6'>
                         {/* 区域标题和总开关 */}
@@ -632,80 +683,102 @@ const NotificationSettings = ({
                           style={{
                             backgroundColor: 'var(--semi-color-fill-0)',
                             border: '1px solid var(--semi-color-border-light)',
-                            borderColor: 'var(--semi-color-fill-1)'
+                            borderColor: 'var(--semi-color-fill-1)',
                           }}
                         >
-                        <div>
-                          <div className='font-semibold text-base text-gray-900 mb-1'>
-                            {section.title}
+                          <div>
+                            <div className='font-semibold text-base text-gray-900 mb-1'>
+                              {section.title}
+                            </div>
+                            <Typography.Text
+                              type='secondary'
+                              size='small'
+                              style={{
+                                fontSize: '12px',
+                                lineHeight: '1.5',
+                                color: 'var(--semi-color-text-2)',
+                              }}
+                            >
+                              {section.description}
+                            </Typography.Text>
                           </div>
-                          <Typography.Text
-                            type="secondary"
-                            size="small"
-                            style={{
-                              fontSize: '12px',
-                              lineHeight: '1.5',
-                              color: 'var(--semi-color-text-2)'
-                            }}
-                          >
-                            {section.description}
-                          </Typography.Text>
+                          <Switch
+                            checked={sidebarModulesUser[section.key]?.enabled}
+                            onChange={handleSectionChange(section.key)}
+                            size='default'
+                          />
                         </div>
-                        <Switch
-                          checked={sidebarModulesUser[section.key]?.enabled}
-                          onChange={handleSectionChange(section.key)}
-                          size="default"
-                        />
-                      </div>
 
-                      {/* 功能模块网格 */}
-                      <Row gutter={[12, 12]}>
-                        {section.modules
-                          .filter(module => isAllowedByAdmin(section.key, module.key))
-                          .map((module) => (
-                          <Col key={module.key} xs={24} sm={24} md={12} lg={8} xl={8}>
-                            <Card
-                              className={`!rounded-xl border border-gray-200 hover:border-blue-300 transition-all duration-200 ${
-                                sidebarModulesUser[section.key]?.enabled ? '' : 'opacity-50'
-                              }`}
-                              bodyStyle={{ padding: '16px' }}
-                              hoverable
-                            >
-                              <div className='flex justify-between items-center h-full'>
-                                <div className='flex-1 text-left'>
-                                  <div className='font-semibold text-sm text-gray-900 mb-1'>
-                                    {module.title}
+                        {/* 功能模块网格 */}
+                        <Row gutter={[12, 12]}>
+                          {section.modules
+                            .filter((module) =>
+                              isAllowedByAdmin(section.key, module.key),
+                            )
+                            .map((module) => (
+                              <Col
+                                key={module.key}
+                                xs={24}
+                                sm={24}
+                                md={12}
+                                lg={8}
+                                xl={8}
+                              >
+                                <Card
+                                  className={`!rounded-xl border border-gray-200 hover:border-blue-300 transition-all duration-200 ${
+                                    sidebarModulesUser[section.key]?.enabled
+                                      ? ''
+                                      : 'opacity-50'
+                                  }`}
+                                  bodyStyle={{ padding: '16px' }}
+                                  hoverable
+                                >
+                                  <div className='flex justify-between items-center h-full'>
+                                    <div className='flex-1 text-left'>
+                                      <div className='font-semibold text-sm text-gray-900 mb-1'>
+                                        {module.title}
+                                      </div>
+                                      <Typography.Text
+                                        type='secondary'
+                                        size='small'
+                                        className='block'
+                                        style={{
+                                          fontSize: '12px',
+                                          lineHeight: '1.5',
+                                          color: 'var(--semi-color-text-2)',
+                                          marginTop: '4px',
+                                        }}
+                                      >
+                                        {module.description}
+                                      </Typography.Text>
+                                    </div>
+                                    <div className='ml-4'>
+                                      <Switch
+                                        checked={
+                                          sidebarModulesUser[section.key]?.[
+                                            module.key
+                                          ]
+                                        }
+                                        onChange={handleModuleChange(
+                                          section.key,
+                                          module.key,
+                                        )}
+                                        size='default'
+                                        disabled={
+                                          !sidebarModulesUser[section.key]
+                                            ?.enabled
+                                        }
+                                      />
+                                    </div>
                                   </div>
-                                  <Typography.Text
-                                    type="secondary"
-                                    size="small"
-                                    className='block'
-                                    style={{
-                                      fontSize: '12px',
-                                      lineHeight: '1.5',
-                                      color: 'var(--semi-color-text-2)',
-                                      marginTop: '4px'
-                                    }}
-                                  >
-                                    {module.description}
-                                  </Typography.Text>
-                                </div>
-                                <div className='ml-4'>
-                                  <Switch
-                                    checked={sidebarModulesUser[section.key]?.[module.key]}
-                                    onChange={handleModuleChange(section.key, module.key)}
-                                    size="default"
-                                    disabled={!sidebarModulesUser[section.key]?.enabled}
-                                  />
-                                </div>
-                              </div>
-                            </Card>
-                          </Col>
-                        ))}
-                      </Row>
+                                </Card>
+                              </Col>
+                            ))}
+                        </Row>
                       </div>
                     ))}
-                  </div> {/* 关闭边栏设置功能区域容器 */}
+                  </div>{' '}
+                  {/* 关闭边栏设置功能区域容器 */}
                 </div>
               </TabPane>
             )}

+ 8 - 5
web/src/components/table/channels/ChannelsColumnDefs.jsx

@@ -231,11 +231,14 @@ export const getChannelsColumns = ({
                     theme='outline'
                     onClick={(e) => {
                       e.stopPropagation();
-                      navigator.clipboard.writeText(record.remark).then(() => {
-                        showSuccess(t('复制成功'));
-                      }).catch(() => {
-                        showError(t('复制失败'));
-                      });
+                      navigator.clipboard
+                        .writeText(record.remark)
+                        .then(() => {
+                          showSuccess(t('复制成功'));
+                        })
+                        .catch(() => {
+                          showError(t('复制失败'));
+                        });
                     }}
                   >
                     {t('复制')}

+ 1 - 1
web/src/hooks/common/useHeaderBar.js

@@ -64,7 +64,7 @@ export const useHeaderBar = ({ onMobileMenuToggle, drawerOpen }) => {
         if (typeof modules.pricing === 'boolean') {
           modules.pricing = {
             enabled: modules.pricing,
-            requireAuth: false  // 默认不需要登录鉴权
+            requireAuth: false, // 默认不需要登录鉴权
           };
         }
 

+ 57 - 58
web/src/hooks/common/useNavigation.js

@@ -20,67 +20,66 @@ For commercial licensing, please contact support@quantumnous.com
 import { useMemo } from 'react';
 
 export const useNavigation = (t, docsLink, headerNavModules) => {
-  const mainNavLinks = useMemo(
-    () => {
-      // 默认配置,如果没有传入配置则显示所有模块
-      const defaultModules = {
-        home: true,
-        console: true,
-        pricing: true,
-        docs: true,
-        about: true,
-      };
+  const mainNavLinks = useMemo(() => {
+    // 默认配置,如果没有传入配置则显示所有模块
+    const defaultModules = {
+      home: true,
+      console: true,
+      pricing: true,
+      docs: true,
+      about: true,
+    };
 
-      // 使用传入的配置或默认配置
-      const modules = headerNavModules || defaultModules;
+    // 使用传入的配置或默认配置
+    const modules = headerNavModules || defaultModules;
 
-      const allLinks = [
-        {
-          text: t('首页'),
-          itemKey: 'home',
-          to: '/',
-        },
-        {
-          text: t('控制台'),
-          itemKey: 'console',
-          to: '/console',
-        },
-        {
-          text: t('模型广场'),
-          itemKey: 'pricing',
-          to: '/pricing',
-        },
-        ...(docsLink
-          ? [
-              {
-                text: t('文档'),
-                itemKey: 'docs',
-                isExternal: true,
-                externalLink: docsLink,
-              },
-            ]
-          : []),
-        {
-          text: t('关于'),
-          itemKey: 'about',
-          to: '/about',
-        },
-      ];
+    const allLinks = [
+      {
+        text: t('首页'),
+        itemKey: 'home',
+        to: '/',
+      },
+      {
+        text: t('控制台'),
+        itemKey: 'console',
+        to: '/console',
+      },
+      {
+        text: t('模型广场'),
+        itemKey: 'pricing',
+        to: '/pricing',
+      },
+      ...(docsLink
+        ? [
+            {
+              text: t('文档'),
+              itemKey: 'docs',
+              isExternal: true,
+              externalLink: docsLink,
+            },
+          ]
+        : []),
+      {
+        text: t('关于'),
+        itemKey: 'about',
+        to: '/about',
+      },
+    ];
 
-      // 根据配置过滤导航链接
-      return allLinks.filter(link => {
-        if (link.itemKey === 'docs') {
-          return docsLink && modules.docs;
-        }
-        if (link.itemKey === 'pricing') {
-          // 支持新的pricing配置格式
-          return typeof modules.pricing === 'object' ? modules.pricing.enabled : modules.pricing;
-        }
-        return modules[link.itemKey] === true;
-      });
-    },
-    [t, docsLink, headerNavModules],
-  );
+    // 根据配置过滤导航链接
+    return allLinks.filter((link) => {
+      if (link.itemKey === 'docs') {
+        return docsLink && modules.docs;
+      }
+      if (link.itemKey === 'pricing') {
+        // 支持新的pricing配置格式
+        return typeof modules.pricing === 'object'
+          ? modules.pricing.enabled
+          : modules.pricing;
+      }
+      return modules[link.itemKey] === true;
+    });
+  }, [t, docsLink, headerNavModules]);
 
   return {
     mainNavLinks,

+ 29 - 22
web/src/hooks/common/useSidebar.js

@@ -31,7 +31,7 @@ export const useSidebar = () => {
     chat: {
       enabled: true,
       playground: true,
-      chat: true
+      chat: true,
     },
     console: {
       enabled: true,
@@ -39,12 +39,12 @@ export const useSidebar = () => {
       token: true,
       log: true,
       midjourney: true,
-      task: true
+      task: true,
     },
     personal: {
       enabled: true,
       topup: true,
-      personal: true
+      personal: true,
     },
     admin: {
       enabled: true,
@@ -52,8 +52,8 @@ export const useSidebar = () => {
       models: true,
       redemption: true,
       user: true,
-      setting: true
-    }
+      setting: true,
+    },
   };
 
   // 获取管理员配置
@@ -87,12 +87,15 @@ export const useSidebar = () => {
         // 当用户没有配置时,生成一个基于管理员配置的默认用户配置
         // 这样可以确保权限控制正确生效
         const defaultUserConfig = {};
-        Object.keys(adminConfig).forEach(sectionKey => {
+        Object.keys(adminConfig).forEach((sectionKey) => {
           if (adminConfig[sectionKey]?.enabled) {
             defaultUserConfig[sectionKey] = { enabled: true };
             // 为每个管理员允许的模块设置默认值为true
-            Object.keys(adminConfig[sectionKey]).forEach(moduleKey => {
-              if (moduleKey !== 'enabled' && adminConfig[sectionKey][moduleKey]) {
+            Object.keys(adminConfig[sectionKey]).forEach((moduleKey) => {
+              if (
+                moduleKey !== 'enabled' &&
+                adminConfig[sectionKey][moduleKey]
+              ) {
                 defaultUserConfig[sectionKey][moduleKey] = true;
               }
             });
@@ -103,10 +106,10 @@ export const useSidebar = () => {
     } catch (error) {
       // 出错时也生成默认配置,而不是设置为空对象
       const defaultUserConfig = {};
-      Object.keys(adminConfig).forEach(sectionKey => {
+      Object.keys(adminConfig).forEach((sectionKey) => {
         if (adminConfig[sectionKey]?.enabled) {
           defaultUserConfig[sectionKey] = { enabled: true };
-          Object.keys(adminConfig[sectionKey]).forEach(moduleKey => {
+          Object.keys(adminConfig[sectionKey]).forEach((moduleKey) => {
             if (moduleKey !== 'enabled' && adminConfig[sectionKey][moduleKey]) {
               defaultUserConfig[sectionKey][moduleKey] = true;
             }
@@ -149,7 +152,7 @@ export const useSidebar = () => {
     }
 
     // 遍历所有区域
-    Object.keys(adminConfig).forEach(sectionKey => {
+    Object.keys(adminConfig).forEach((sectionKey) => {
       const adminSection = adminConfig[sectionKey];
       const userSection = userConfig[sectionKey];
 
@@ -161,18 +164,21 @@ export const useSidebar = () => {
 
       // 区域级别:用户可以选择隐藏管理员允许的区域
       // 当userSection存在时检查enabled状态,否则默认为true
-      const sectionEnabled = userSection ? (userSection.enabled !== false) : true;
+      const sectionEnabled = userSection ? userSection.enabled !== false : true;
       result[sectionKey] = { enabled: sectionEnabled };
 
       // 功能级别:只有管理员和用户都允许的功能才显示
-      Object.keys(adminSection).forEach(moduleKey => {
+      Object.keys(adminSection).forEach((moduleKey) => {
         if (moduleKey === 'enabled') return;
 
         const adminAllowed = adminSection[moduleKey];
         // 当userSection存在时检查模块状态,否则默认为true
-        const userAllowed = userSection ? (userSection[moduleKey] !== false) : true;
+        const userAllowed = userSection
+          ? userSection[moduleKey] !== false
+          : true;
 
-        result[sectionKey][moduleKey] = adminAllowed && userAllowed && sectionEnabled;
+        result[sectionKey][moduleKey] =
+          adminAllowed && userAllowed && sectionEnabled;
       });
     });
 
@@ -192,9 +198,9 @@ export const useSidebar = () => {
   const hasSectionVisibleModules = (sectionKey) => {
     const section = finalConfig[sectionKey];
     if (!section?.enabled) return false;
-    
-    return Object.keys(section).some(key => 
-      key !== 'enabled' && section[key] === true
+
+    return Object.keys(section).some(
+      (key) => key !== 'enabled' && section[key] === true,
     );
   };
 
@@ -202,9 +208,10 @@ export const useSidebar = () => {
   const getVisibleModules = (sectionKey) => {
     const section = finalConfig[sectionKey];
     if (!section?.enabled) return [];
-    
-    return Object.keys(section)
-      .filter(key => key !== 'enabled' && section[key] === true);
+
+    return Object.keys(section).filter(
+      (key) => key !== 'enabled' && section[key] === true,
+    );
   };
 
   return {
@@ -215,6 +222,6 @@ export const useSidebar = () => {
     isModuleVisible,
     hasSectionVisibleModules,
     getVisibleModules,
-    refreshUserConfig
+    refreshUserConfig,
   };
 };

+ 29 - 10
web/src/hooks/common/useUserPermissions.js

@@ -1,3 +1,21 @@
+/*
+Copyright (C) 2025 QuantumNous
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+For commercial licensing, please contact support@quantumnous.com
+*/
 import { useState, useEffect } from 'react';
 import { API } from '../../helpers';
 
@@ -52,22 +70,22 @@ export const useUserPermissions = () => {
   const isSidebarModuleAllowed = (sectionKey, moduleKey) => {
     if (!permissions?.sidebar_modules) return true;
     const sectionPerms = permissions.sidebar_modules[sectionKey];
-    
+
     // 如果整个区域被禁用
     if (sectionPerms === false) return false;
-    
+
     // 如果区域存在但模块被禁用
     if (sectionPerms && sectionPerms[moduleKey] === false) return false;
-    
+
     return true;
   };
 
   // 获取允许的边栏区域列表
   const getAllowedSidebarSections = () => {
     if (!permissions?.sidebar_modules) return [];
-    
-    return Object.keys(permissions.sidebar_modules).filter(sectionKey => 
-      isSidebarSectionAllowed(sectionKey)
+
+    return Object.keys(permissions.sidebar_modules).filter((sectionKey) =>
+      isSidebarSectionAllowed(sectionKey),
     );
   };
 
@@ -75,12 +93,13 @@ export const useUserPermissions = () => {
   const getAllowedSidebarModules = (sectionKey) => {
     if (!permissions?.sidebar_modules) return [];
     const sectionPerms = permissions.sidebar_modules[sectionKey];
-    
+
     if (sectionPerms === false) return [];
     if (!sectionPerms || typeof sectionPerms !== 'object') return [];
-    
-    return Object.keys(sectionPerms).filter(moduleKey => 
-      moduleKey !== 'enabled' && sectionPerms[moduleKey] === true
+
+    return Object.keys(sectionPerms).filter(
+      (moduleKey) =>
+        moduleKey !== 'enabled' && sectionPerms[moduleKey] === true,
     );
   };
 

+ 117 - 87
web/src/pages/Setting/Operation/SettingsHeaderNavModules.jsx

@@ -18,7 +18,15 @@ For commercial licensing, please contact support@quantumnous.com
 */
 
 import React, { useEffect, useState, useContext } from 'react';
-import { Button, Card, Col, Form, Row, Switch, Typography } from '@douyinfe/semi-ui';
+import {
+  Button,
+  Card,
+  Col,
+  Form,
+  Row,
+  Switch,
+  Typography,
+} from '@douyinfe/semi-ui';
 import { API, showError, showSuccess } from '../../../helpers';
 import { useTranslation } from 'react-i18next';
 import { StatusContext } from '../../../context/Status';
@@ -29,14 +37,14 @@ export default function SettingsHeaderNavModules(props) {
   const { t } = useTranslation();
   const [loading, setLoading] = useState(false);
   const [statusState, statusDispatch] = useContext(StatusContext);
-  
+
   // 顶栏模块管理状态
   const [headerNavModules, setHeaderNavModules] = useState({
     home: true,
     console: true,
     pricing: {
       enabled: true,
-      requireAuth: false  // 默认不需要登录鉴权
+      requireAuth: false, // 默认不需要登录鉴权
     },
     docs: true,
     about: true,
@@ -50,7 +58,7 @@ export default function SettingsHeaderNavModules(props) {
         // 对于pricing模块,只更新enabled属性
         newModules[moduleKey] = {
           ...newModules[moduleKey],
-          enabled: checked
+          enabled: checked,
         };
       } else {
         newModules[moduleKey] = checked;
@@ -64,7 +72,7 @@ export default function SettingsHeaderNavModules(props) {
     const newModules = { ...headerNavModules };
     newModules.pricing = {
       ...newModules.pricing,
-      requireAuth: checked
+      requireAuth: checked,
     };
     setHeaderNavModules(newModules);
   }
@@ -76,7 +84,7 @@ export default function SettingsHeaderNavModules(props) {
       console: true,
       pricing: {
         enabled: true,
-        requireAuth: false
+        requireAuth: false,
       },
       docs: true,
       about: true,
@@ -102,8 +110,8 @@ export default function SettingsHeaderNavModules(props) {
           type: 'set',
           payload: {
             ...statusState.status,
-            HeaderNavModules: JSON.stringify(headerNavModules)
-          }
+            HeaderNavModules: JSON.stringify(headerNavModules),
+          },
         });
 
         // 刷新父组件状态
@@ -130,7 +138,7 @@ export default function SettingsHeaderNavModules(props) {
         if (typeof modules.pricing === 'boolean') {
           modules.pricing = {
             enabled: modules.pricing,
-            requireAuth: false  // 默认不需要登录鉴权
+            requireAuth: false, // 默认不需要登录鉴权
           };
         }
 
@@ -142,7 +150,7 @@ export default function SettingsHeaderNavModules(props) {
           console: true,
           pricing: {
             enabled: true,
-            requireAuth: false
+            requireAuth: false,
           },
           docs: true,
           about: true,
@@ -157,35 +165,37 @@ export default function SettingsHeaderNavModules(props) {
     {
       key: 'home',
       title: t('首页'),
-      description: t('用户主页,展示系统信息')
+      description: t('用户主页,展示系统信息'),
     },
     {
       key: 'console',
       title: t('控制台'),
-      description: t('用户控制面板,管理账户')
+      description: t('用户控制面板,管理账户'),
     },
     {
       key: 'pricing',
       title: t('模型广场'),
       description: t('模型定价,需要登录访问'),
-      hasSubConfig: true  // 标识该模块有子配置
+      hasSubConfig: true, // 标识该模块有子配置
     },
     {
       key: 'docs',
       title: t('文档'),
-      description: t('系统文档和帮助信息')
+      description: t('系统文档和帮助信息'),
     },
     {
       key: 'about',
       title: t('关于'),
-      description: t('关于系统的详细信息')
-    }
+      description: t('关于系统的详细信息'),
+    },
   ];
 
   return (
     <Card>
-      <Form.Section text={t('顶栏管理')} extraText={t('控制顶栏模块显示状态,全局生效')}>
-
+      <Form.Section
+        text={t('顶栏管理')}
+        extraText={t('控制顶栏模块显示状态,全局生效')}
+      >
         <Row gutter={[16, 16]} style={{ marginBottom: '24px' }}>
           {moduleConfigs.map((module) => (
             <Col key={module.key} xs={24} sm={12} md={6} lg={6} xl={6}>
@@ -195,34 +205,38 @@ export default function SettingsHeaderNavModules(props) {
                   border: '1px solid var(--semi-color-border)',
                   transition: 'all 0.2s ease',
                   background: 'var(--semi-color-bg-1)',
-                  minHeight: '80px'
+                  minHeight: '80px',
                 }}
                 bodyStyle={{ padding: '16px' }}
                 hoverable
               >
-                <div style={{
-                  display: 'flex',
-                  justifyContent: 'space-between',
-                  alignItems: 'center',
-                  height: '100%'
-                }}>
+                <div
+                  style={{
+                    display: 'flex',
+                    justifyContent: 'space-between',
+                    alignItems: 'center',
+                    height: '100%',
+                  }}
+                >
                   <div style={{ flex: 1, textAlign: 'left' }}>
-                    <div style={{
-                      fontWeight: '600',
-                      fontSize: '14px',
-                      color: 'var(--semi-color-text-0)',
-                      marginBottom: '4px'
-                    }}>
+                    <div
+                      style={{
+                        fontWeight: '600',
+                        fontSize: '14px',
+                        color: 'var(--semi-color-text-0)',
+                        marginBottom: '4px',
+                      }}
+                    >
                       {module.title}
                     </div>
                     <Text
-                      type="secondary"
-                      size="small"
+                      type='secondary'
+                      size='small'
                       style={{
                         fontSize: '12px',
                         color: 'var(--semi-color-text-2)',
                         lineHeight: '1.4',
-                        display: 'block'
+                        display: 'block',
                       }}
                     >
                       {module.description}
@@ -230,78 +244,94 @@ export default function SettingsHeaderNavModules(props) {
                   </div>
                   <div style={{ marginLeft: '16px' }}>
                     <Switch
-                      checked={module.key === 'pricing' ? headerNavModules[module.key]?.enabled : headerNavModules[module.key]}
+                      checked={
+                        module.key === 'pricing'
+                          ? headerNavModules[module.key]?.enabled
+                          : headerNavModules[module.key]
+                      }
                       onChange={handleHeaderNavModuleChange(module.key)}
-                      size="default"
+                      size='default'
                     />
                   </div>
                 </div>
 
                 {/* 为模型广场添加权限控制子开关 */}
-                {module.key === 'pricing' && (module.key === 'pricing' ? headerNavModules[module.key]?.enabled : headerNavModules[module.key]) && (
-                  <div style={{
-                    borderTop: '1px solid var(--semi-color-border)',
-                    marginTop: '12px',
-                    paddingTop: '12px'
-                  }}>
-                    <div style={{
-                      display: 'flex',
-                      justifyContent: 'space-between',
-                      alignItems: 'center'
-                    }}>
-                      <div style={{ flex: 1, textAlign: 'left' }}>
-                        <div style={{
-                          fontWeight: '500',
-                          fontSize: '12px',
-                          color: 'var(--semi-color-text-1)',
-                          marginBottom: '2px'
-                        }}>
-                          {t('需要登录访问')}
+                {module.key === 'pricing' &&
+                  (module.key === 'pricing'
+                    ? headerNavModules[module.key]?.enabled
+                    : headerNavModules[module.key]) && (
+                    <div
+                      style={{
+                        borderTop: '1px solid var(--semi-color-border)',
+                        marginTop: '12px',
+                        paddingTop: '12px',
+                      }}
+                    >
+                      <div
+                        style={{
+                          display: 'flex',
+                          justifyContent: 'space-between',
+                          alignItems: 'center',
+                        }}
+                      >
+                        <div style={{ flex: 1, textAlign: 'left' }}>
+                          <div
+                            style={{
+                              fontWeight: '500',
+                              fontSize: '12px',
+                              color: 'var(--semi-color-text-1)',
+                              marginBottom: '2px',
+                            }}
+                          >
+                            {t('需要登录访问')}
+                          </div>
+                          <Text
+                            type='secondary'
+                            size='small'
+                            style={{
+                              fontSize: '11px',
+                              color: 'var(--semi-color-text-2)',
+                              lineHeight: '1.4',
+                              display: 'block',
+                            }}
+                          >
+                            {t('开启后未登录用户无法访问模型广场')}
+                          </Text>
+                        </div>
+                        <div style={{ marginLeft: '16px' }}>
+                          <Switch
+                            checked={
+                              headerNavModules.pricing?.requireAuth || false
+                            }
+                            onChange={handlePricingAuthChange}
+                            size='small'
+                          />
                         </div>
-                        <Text
-                          type="secondary"
-                          size="small"
-                          style={{
-                            fontSize: '11px',
-                            color: 'var(--semi-color-text-2)',
-                            lineHeight: '1.4',
-                            display: 'block'
-                          }}
-                        >
-                          {t('开启后未登录用户无法访问模型广场')}
-                        </Text>
-                      </div>
-                      <div style={{ marginLeft: '16px' }}>
-                        <Switch
-                          checked={headerNavModules.pricing?.requireAuth || false}
-                          onChange={handlePricingAuthChange}
-                          size="small"
-                        />
                       </div>
                     </div>
-                  </div>
-                )}
+                  )}
               </Card>
             </Col>
           ))}
-
         </Row>
 
-        <div style={{
-          display: 'flex',
-          gap: '12px',
-          justifyContent: 'flex-start',
-          alignItems: 'center',
-          paddingTop: '8px',
-          borderTop: '1px solid var(--semi-color-border)'
-        }}>
+        <div
+          style={{
+            display: 'flex',
+            gap: '12px',
+            justifyContent: 'flex-start',
+            alignItems: 'center',
+            paddingTop: '8px',
+            borderTop: '1px solid var(--semi-color-border)',
+          }}
+        >
           <Button
             size='default'
             type='tertiary'
             onClick={resetHeaderNavModules}
             style={{
               borderRadius: '6px',
-              fontWeight: '500'
+              fontWeight: '500',
             }}
           >
             {t('重置为默认')}
@@ -314,7 +344,7 @@ export default function SettingsHeaderNavModules(props) {
             style={{
               borderRadius: '6px',
               fontWeight: '500',
-              minWidth: '100px'
+              minWidth: '100px',
             }}
           >
             {t('保存设置')}

+ 151 - 91
web/src/pages/Setting/Operation/SettingsSidebarModulesAdmin.jsx

@@ -19,7 +19,15 @@ For commercial licensing, please contact support@quantumnous.com
 
 import React, { useState, useEffect, useContext } from 'react';
 import { useTranslation } from 'react-i18next';
-import { Card, Form, Button, Switch, Row, Col, Typography } from '@douyinfe/semi-ui';
+import {
+  Card,
+  Form,
+  Button,
+  Switch,
+  Row,
+  Col,
+  Typography,
+} from '@douyinfe/semi-ui';
 import { API, showSuccess, showError } from '../../../helpers';
 import { StatusContext } from '../../../context/Status';
 
@@ -29,13 +37,13 @@ export default function SettingsSidebarModulesAdmin(props) {
   const { t } = useTranslation();
   const [loading, setLoading] = useState(false);
   const [statusState, statusDispatch] = useContext(StatusContext);
-  
+
   // 左侧边栏模块管理状态(管理员全局控制)
   const [sidebarModulesAdmin, setSidebarModulesAdmin] = useState({
     chat: {
       enabled: true,
       playground: true,
-      chat: true
+      chat: true,
     },
     console: {
       enabled: true,
@@ -43,12 +51,12 @@ export default function SettingsSidebarModulesAdmin(props) {
       token: true,
       log: true,
       midjourney: true,
-      task: true
+      task: true,
     },
     personal: {
       enabled: true,
       topup: true,
-      personal: true
+      personal: true,
     },
     admin: {
       enabled: true,
@@ -56,19 +64,19 @@ export default function SettingsSidebarModulesAdmin(props) {
       models: true,
       redemption: true,
       user: true,
-      setting: true
-    }
+      setting: true,
+    },
   });
 
   // 处理区域级别开关变更
   function handleSectionChange(sectionKey) {
     return (checked) => {
-      const newModules = { 
-        ...sidebarModulesAdmin, 
-        [sectionKey]: { 
-          ...sidebarModulesAdmin[sectionKey], 
-          enabled: checked 
-        } 
+      const newModules = {
+        ...sidebarModulesAdmin,
+        [sectionKey]: {
+          ...sidebarModulesAdmin[sectionKey],
+          enabled: checked,
+        },
       };
       setSidebarModulesAdmin(newModules);
     };
@@ -77,12 +85,12 @@ export default function SettingsSidebarModulesAdmin(props) {
   // 处理功能级别开关变更
   function handleModuleChange(sectionKey, moduleKey) {
     return (checked) => {
-      const newModules = { 
-        ...sidebarModulesAdmin, 
-        [sectionKey]: { 
-          ...sidebarModulesAdmin[sectionKey], 
-          [moduleKey]: checked 
-        } 
+      const newModules = {
+        ...sidebarModulesAdmin,
+        [sectionKey]: {
+          ...sidebarModulesAdmin[sectionKey],
+          [moduleKey]: checked,
+        },
       };
       setSidebarModulesAdmin(newModules);
     };
@@ -94,7 +102,7 @@ export default function SettingsSidebarModulesAdmin(props) {
       chat: {
         enabled: true,
         playground: true,
-        chat: true
+        chat: true,
       },
       console: {
         enabled: true,
@@ -102,12 +110,12 @@ export default function SettingsSidebarModulesAdmin(props) {
         token: true,
         log: true,
         midjourney: true,
-        task: true
+        task: true,
       },
       personal: {
         enabled: true,
         topup: true,
-        personal: true
+        personal: true,
       },
       admin: {
         enabled: true,
@@ -115,8 +123,8 @@ export default function SettingsSidebarModulesAdmin(props) {
         models: true,
         redemption: true,
         user: true,
-        setting: true
-      }
+        setting: true,
+      },
     };
     setSidebarModulesAdmin(defaultModules);
     showSuccess(t('已重置为默认配置'));
@@ -139,8 +147,8 @@ export default function SettingsSidebarModulesAdmin(props) {
           type: 'set',
           payload: {
             ...statusState.status,
-            SidebarModulesAdmin: JSON.stringify(sidebarModulesAdmin)
-          }
+            SidebarModulesAdmin: JSON.stringify(sidebarModulesAdmin),
+          },
         });
 
         // 刷新父组件状态
@@ -167,9 +175,23 @@ export default function SettingsSidebarModulesAdmin(props) {
         // 使用默认配置
         const defaultModules = {
           chat: { enabled: true, playground: true, chat: true },
-          console: { enabled: true, detail: true, token: true, log: true, midjourney: true, task: true },
+          console: {
+            enabled: true,
+            detail: true,
+            token: true,
+            log: true,
+            midjourney: true,
+            task: true,
+          },
           personal: { enabled: true, topup: true, personal: true },
-          admin: { enabled: true, channel: true, models: true, redemption: true, user: true, setting: true }
+          admin: {
+            enabled: true,
+            channel: true,
+            models: true,
+            redemption: true,
+            user: true,
+            setting: true,
+          },
         };
         setSidebarModulesAdmin(defaultModules);
       }
@@ -183,9 +205,13 @@ export default function SettingsSidebarModulesAdmin(props) {
       title: t('聊天区域'),
       description: t('操练场和聊天功能'),
       modules: [
-        { key: 'playground', title: t('操练场'), description: t('AI模型测试环境') },
-        { key: 'chat', title: t('聊天'), description: t('聊天会话管理') }
-      ]
+        {
+          key: 'playground',
+          title: t('操练场'),
+          description: t('AI模型测试环境'),
+        },
+        { key: 'chat', title: t('聊天'), description: t('聊天会话管理') },
+      ],
     },
     {
       key: 'console',
@@ -195,9 +221,13 @@ export default function SettingsSidebarModulesAdmin(props) {
         { key: 'detail', title: t('数据看板'), description: t('系统数据统计') },
         { key: 'token', title: t('令牌管理'), description: t('API令牌管理') },
         { key: 'log', title: t('使用日志'), description: t('API使用记录') },
-        { key: 'midjourney', title: t('绘图日志'), description: t('绘图任务记录') },
-        { key: 'task', title: t('任务日志'), description: t('系统任务记录') }
-      ]
+        {
+          key: 'midjourney',
+          title: t('绘图日志'),
+          description: t('绘图任务记录'),
+        },
+        { key: 'task', title: t('任务日志'), description: t('系统任务记录') },
+      ],
     },
     {
       key: 'personal',
@@ -205,8 +235,12 @@ export default function SettingsSidebarModulesAdmin(props) {
       description: t('用户个人功能'),
       modules: [
         { key: 'topup', title: t('钱包管理'), description: t('余额充值管理') },
-        { key: 'personal', title: t('个人设置'), description: t('个人信息设置') }
-      ]
+        {
+          key: 'personal',
+          title: t('个人设置'),
+          description: t('个人信息设置'),
+        },
+      ],
     },
     {
       key: 'admin',
@@ -215,46 +249,62 @@ export default function SettingsSidebarModulesAdmin(props) {
       modules: [
         { key: 'channel', title: t('渠道管理'), description: t('API渠道配置') },
         { key: 'models', title: t('模型管理'), description: t('AI模型配置') },
-        { key: 'redemption', title: t('兑换码管理'), description: t('兑换码生成管理') },
+        {
+          key: 'redemption',
+          title: t('兑换码管理'),
+          description: t('兑换码生成管理'),
+        },
         { key: 'user', title: t('用户管理'), description: t('用户账户管理') },
-        { key: 'setting', title: t('系统设置'), description: t('系统参数配置') }
-      ]
-    }
+        {
+          key: 'setting',
+          title: t('系统设置'),
+          description: t('系统参数配置'),
+        },
+      ],
+    },
   ];
 
   return (
     <Card>
-      <Form.Section text={t('侧边栏管理(全局控制)')} extraText={t('全局控制侧边栏区域和功能显示,管理员隐藏的功能用户无法启用')}>
-
+      <Form.Section
+        text={t('侧边栏管理(全局控制)')}
+        extraText={t(
+          '全局控制侧边栏区域和功能显示,管理员隐藏的功能用户无法启用',
+        )}
+      >
         {sectionConfigs.map((section) => (
           <div key={section.key} style={{ marginBottom: '32px' }}>
             {/* 区域标题和总开关 */}
-            <div style={{
-              display: 'flex',
-              justifyContent: 'space-between',
-              alignItems: 'center',
-              marginBottom: '16px',
-              padding: '12px 16px',
-              backgroundColor: 'var(--semi-color-fill-0)',
-              borderRadius: '8px',
-              border: '1px solid var(--semi-color-border)'
-            }}>
+            <div
+              style={{
+                display: 'flex',
+                justifyContent: 'space-between',
+                alignItems: 'center',
+                marginBottom: '16px',
+                padding: '12px 16px',
+                backgroundColor: 'var(--semi-color-fill-0)',
+                borderRadius: '8px',
+                border: '1px solid var(--semi-color-border)',
+              }}
+            >
               <div>
-                <div style={{
-                  fontWeight: '600',
-                  fontSize: '16px',
-                  color: 'var(--semi-color-text-0)',
-                  marginBottom: '4px'
-                }}>
+                <div
+                  style={{
+                    fontWeight: '600',
+                    fontSize: '16px',
+                    color: 'var(--semi-color-text-0)',
+                    marginBottom: '4px',
+                  }}
+                >
                   {section.title}
                 </div>
                 <Text
-                  type="secondary"
-                  size="small"
+                  type='secondary'
+                  size='small'
                   style={{
                     fontSize: '12px',
                     color: 'var(--semi-color-text-2)',
-                    lineHeight: '1.4'
+                    lineHeight: '1.4',
                   }}
                 >
                   {section.description}
@@ -263,7 +313,7 @@ export default function SettingsSidebarModulesAdmin(props) {
               <Switch
                 checked={sidebarModulesAdmin[section.key]?.enabled}
                 onChange={handleSectionChange(section.key)}
-                size="default"
+                size='default'
               />
             </div>
 
@@ -275,33 +325,39 @@ export default function SettingsSidebarModulesAdmin(props) {
                     bodyStyle={{ padding: '16px' }}
                     hoverable
                     style={{
-                      opacity: sidebarModulesAdmin[section.key]?.enabled ? 1 : 0.5,
-                      transition: 'opacity 0.2s'
+                      opacity: sidebarModulesAdmin[section.key]?.enabled
+                        ? 1
+                        : 0.5,
+                      transition: 'opacity 0.2s',
                     }}
                   >
-                    <div style={{
-                      display: 'flex',
-                      justifyContent: 'space-between',
-                      alignItems: 'center',
-                      height: '100%'
-                    }}>
+                    <div
+                      style={{
+                        display: 'flex',
+                        justifyContent: 'space-between',
+                        alignItems: 'center',
+                        height: '100%',
+                      }}
+                    >
                       <div style={{ flex: 1, textAlign: 'left' }}>
-                        <div style={{
-                          fontWeight: '600',
-                          fontSize: '14px',
-                          color: 'var(--semi-color-text-0)',
-                          marginBottom: '4px'
-                        }}>
+                        <div
+                          style={{
+                            fontWeight: '600',
+                            fontSize: '14px',
+                            color: 'var(--semi-color-text-0)',
+                            marginBottom: '4px',
+                          }}
+                        >
                           {module.title}
                         </div>
                         <Text
-                          type="secondary"
-                          size="small"
+                          type='secondary'
+                          size='small'
                           style={{
                             fontSize: '12px',
                             color: 'var(--semi-color-text-2)',
                             lineHeight: '1.4',
-                            display: 'block'
+                            display: 'block',
                           }}
                         >
                           {module.description}
@@ -309,9 +365,11 @@ export default function SettingsSidebarModulesAdmin(props) {
                       </div>
                       <div style={{ marginLeft: '16px' }}>
                         <Switch
-                          checked={sidebarModulesAdmin[section.key]?.[module.key]}
+                          checked={
+                            sidebarModulesAdmin[section.key]?.[module.key]
+                          }
                           onChange={handleModuleChange(section.key, module.key)}
-                          size="default"
+                          size='default'
                           disabled={!sidebarModulesAdmin[section.key]?.enabled}
                         />
                       </div>
@@ -323,21 +381,23 @@ export default function SettingsSidebarModulesAdmin(props) {
           </div>
         ))}
 
-        <div style={{
-          display: 'flex',
-          gap: '12px',
-          justifyContent: 'flex-start',
-          alignItems: 'center',
-          paddingTop: '8px',
-          borderTop: '1px solid var(--semi-color-border)'
-        }}>
+        <div
+          style={{
+            display: 'flex',
+            gap: '12px',
+            justifyContent: 'flex-start',
+            alignItems: 'center',
+            paddingTop: '8px',
+            borderTop: '1px solid var(--semi-color-border)',
+          }}
+        >
           <Button
             size='default'
             type='tertiary'
             onClick={resetSidebarModules}
             style={{
               borderRadius: '6px',
-              fontWeight: '500'
+              fontWeight: '500',
             }}
           >
             {t('重置为默认')}
@@ -350,7 +410,7 @@ export default function SettingsSidebarModulesAdmin(props) {
             style={{
               borderRadius: '6px',
               fontWeight: '500',
-              minWidth: '100px'
+              minWidth: '100px',
             }}
           >
             {t('保存设置')}

+ 148 - 99
web/src/pages/Setting/Personal/SettingsSidebarModulesUser.jsx

@@ -19,7 +19,15 @@ For commercial licensing, please contact support@quantumnous.com
 
 import { useState, useEffect, useContext } from 'react';
 import { useTranslation } from 'react-i18next';
-import { Card, Button, Switch, Typography, Row, Col, Avatar } from '@douyinfe/semi-ui';
+import {
+  Card,
+  Button,
+  Switch,
+  Typography,
+  Row,
+  Col,
+  Avatar,
+} from '@douyinfe/semi-ui';
 import { API, showSuccess, showError } from '../../../helpers';
 import { StatusContext } from '../../../context/Status';
 import { UserContext } from '../../../context/User';
@@ -55,7 +63,7 @@ export default function SettingsSidebarModulesUser() {
   if (permissionsLoading) {
     return null;
   }
-  
+
   // 根据用户权限生成默认配置
   const generateDefaultConfig = () => {
     const defaultConfig = {};
@@ -65,7 +73,7 @@ export default function SettingsSidebarModulesUser() {
       defaultConfig.chat = {
         enabled: true,
         playground: isSidebarModuleAllowed('chat', 'playground'),
-        chat: isSidebarModuleAllowed('chat', 'chat')
+        chat: isSidebarModuleAllowed('chat', 'chat'),
       };
     }
 
@@ -77,7 +85,7 @@ export default function SettingsSidebarModulesUser() {
         token: isSidebarModuleAllowed('console', 'token'),
         log: isSidebarModuleAllowed('console', 'log'),
         midjourney: isSidebarModuleAllowed('console', 'midjourney'),
-        task: isSidebarModuleAllowed('console', 'task')
+        task: isSidebarModuleAllowed('console', 'task'),
       };
     }
 
@@ -86,7 +94,7 @@ export default function SettingsSidebarModulesUser() {
       defaultConfig.personal = {
         enabled: true,
         topup: isSidebarModuleAllowed('personal', 'topup'),
-        personal: isSidebarModuleAllowed('personal', 'personal')
+        personal: isSidebarModuleAllowed('personal', 'personal'),
       };
     }
 
@@ -98,7 +106,7 @@ export default function SettingsSidebarModulesUser() {
         models: isSidebarModuleAllowed('admin', 'models'),
         redemption: isSidebarModuleAllowed('admin', 'redemption'),
         user: isSidebarModuleAllowed('admin', 'user'),
-        setting: isSidebarModuleAllowed('admin', 'setting')
+        setting: isSidebarModuleAllowed('admin', 'setting'),
       };
     }
 
@@ -114,12 +122,12 @@ export default function SettingsSidebarModulesUser() {
   // 处理区域级别开关变更
   function handleSectionChange(sectionKey) {
     return (checked) => {
-      const newModules = { 
-        ...sidebarModulesUser, 
-        [sectionKey]: { 
-          ...sidebarModulesUser[sectionKey], 
-          enabled: checked 
-        } 
+      const newModules = {
+        ...sidebarModulesUser,
+        [sectionKey]: {
+          ...sidebarModulesUser[sectionKey],
+          enabled: checked,
+        },
       };
       setSidebarModulesUser(newModules);
       console.log('用户边栏区域配置变更:', sectionKey, checked, newModules);
@@ -129,15 +137,21 @@ export default function SettingsSidebarModulesUser() {
   // 处理功能级别开关变更
   function handleModuleChange(sectionKey, moduleKey) {
     return (checked) => {
-      const newModules = { 
-        ...sidebarModulesUser, 
-        [sectionKey]: { 
-          ...sidebarModulesUser[sectionKey], 
-          [moduleKey]: checked 
-        } 
+      const newModules = {
+        ...sidebarModulesUser,
+        [sectionKey]: {
+          ...sidebarModulesUser[sectionKey],
+          [moduleKey]: checked,
+        },
       };
       setSidebarModulesUser(newModules);
-      console.log('用户边栏功能配置变更:', sectionKey, moduleKey, checked, newModules);
+      console.log(
+        '用户边栏功能配置变更:',
+        sectionKey,
+        moduleKey,
+        checked,
+        newModules,
+      );
     };
   }
 
@@ -202,12 +216,15 @@ export default function SettingsSidebarModulesUser() {
 
           // 确保用户配置也经过权限过滤
           const filteredUserConf = {};
-          Object.keys(userConf).forEach(sectionKey => {
+          Object.keys(userConf).forEach((sectionKey) => {
             if (isSidebarSectionAllowed(sectionKey)) {
               filteredUserConf[sectionKey] = { ...userConf[sectionKey] };
               // 过滤不允许的模块
-              Object.keys(userConf[sectionKey]).forEach(moduleKey => {
-                if (moduleKey !== 'enabled' && !isSidebarModuleAllowed(sectionKey, moduleKey)) {
+              Object.keys(userConf[sectionKey]).forEach((moduleKey) => {
+                if (
+                  moduleKey !== 'enabled' &&
+                  !isSidebarModuleAllowed(sectionKey, moduleKey)
+                ) {
                   delete filteredUserConf[sectionKey][moduleKey];
                 }
               });
@@ -233,14 +250,22 @@ export default function SettingsSidebarModulesUser() {
     if (!permissionsLoading && hasSidebarSettingsPermission()) {
       loadConfigs();
     }
-  }, [statusState, permissionsLoading, hasSidebarSettingsPermission, isSidebarSectionAllowed, isSidebarModuleAllowed]);
+  }, [
+    statusState,
+    permissionsLoading,
+    hasSidebarSettingsPermission,
+    isSidebarSectionAllowed,
+    isSidebarModuleAllowed,
+  ]);
 
   // 检查功能是否被管理员允许
   const isAllowedByAdmin = (sectionKey, moduleKey = null) => {
     if (!adminConfig) return true;
-    
+
     if (moduleKey) {
-      return adminConfig[sectionKey]?.enabled && adminConfig[sectionKey]?.[moduleKey];
+      return (
+        adminConfig[sectionKey]?.enabled && adminConfig[sectionKey]?.[moduleKey]
+      );
     } else {
       return adminConfig[sectionKey]?.enabled;
     }
@@ -253,9 +278,13 @@ export default function SettingsSidebarModulesUser() {
       title: t('聊天区域'),
       description: t('操练场和聊天功能'),
       modules: [
-        { key: 'playground', title: t('操练场'), description: t('AI模型测试环境') },
-        { key: 'chat', title: t('聊天'), description: t('聊天会话管理') }
-      ]
+        {
+          key: 'playground',
+          title: t('操练场'),
+          description: t('AI模型测试环境'),
+        },
+        { key: 'chat', title: t('聊天'), description: t('聊天会话管理') },
+      ],
     },
     {
       key: 'console',
@@ -265,9 +294,13 @@ export default function SettingsSidebarModulesUser() {
         { key: 'detail', title: t('数据看板'), description: t('系统数据统计') },
         { key: 'token', title: t('令牌管理'), description: t('API令牌管理') },
         { key: 'log', title: t('使用日志'), description: t('API使用记录') },
-        { key: 'midjourney', title: t('绘图日志'), description: t('绘图任务记录') },
-        { key: 'task', title: t('任务日志'), description: t('系统任务记录') }
-      ]
+        {
+          key: 'midjourney',
+          title: t('绘图日志'),
+          description: t('绘图任务记录'),
+        },
+        { key: 'task', title: t('任务日志'), description: t('系统任务记录') },
+      ],
     },
     {
       key: 'personal',
@@ -275,8 +308,12 @@ export default function SettingsSidebarModulesUser() {
       description: t('用户个人功能'),
       modules: [
         { key: 'topup', title: t('钱包管理'), description: t('余额充值管理') },
-        { key: 'personal', title: t('个人设置'), description: t('个人信息设置') }
-      ]
+        {
+          key: 'personal',
+          title: t('个人设置'),
+          description: t('个人信息设置'),
+        },
+      ],
     },
     {
       key: 'admin',
@@ -285,23 +322,35 @@ export default function SettingsSidebarModulesUser() {
       modules: [
         { key: 'channel', title: t('渠道管理'), description: t('API渠道配置') },
         { key: 'models', title: t('模型管理'), description: t('AI模型配置') },
-        { key: 'redemption', title: t('兑换码管理'), description: t('兑换码生成管理') },
+        {
+          key: 'redemption',
+          title: t('兑换码管理'),
+          description: t('兑换码生成管理'),
+        },
         { key: 'user', title: t('用户管理'), description: t('用户账户管理') },
-        { key: 'setting', title: t('系统设置'), description: t('系统参数配置') }
-      ]
-    }
-  ].filter(section => {
-    // 使用后端权限验证替代前端角色判断
-    return isSidebarSectionAllowed(section.key);
-  }).map(section => ({
-    ...section,
-    modules: section.modules.filter(module =>
-      isSidebarModuleAllowed(section.key, module.key)
-    )
-  })).filter(section =>
-    // 过滤掉没有可用模块的区域
-    section.modules.length > 0 && isAllowedByAdmin(section.key)
-  );
+        {
+          key: 'setting',
+          title: t('系统设置'),
+          description: t('系统参数配置'),
+        },
+      ],
+    },
+  ]
+    .filter((section) => {
+      // 使用后端权限验证替代前端角色判断
+      return isSidebarSectionAllowed(section.key);
+    })
+    .map((section) => ({
+      ...section,
+      modules: section.modules.filter((module) =>
+        isSidebarModuleAllowed(section.key, module.key),
+      ),
+    }))
+    .filter(
+      (section) =>
+        // 过滤掉没有可用模块的区域
+        section.modules.length > 0 && isAllowedByAdmin(section.key),
+    );
 
   return (
     <Card className='!rounded-2xl shadow-sm border-0'>
@@ -321,65 +370,65 @@ export default function SettingsSidebarModulesUser() {
       </div>
 
       <div className='mb-4'>
-        <Text type="secondary" className='text-sm text-gray-600'>
+        <Text type='secondary' className='text-sm text-gray-600'>
           {t('您可以个性化设置侧边栏的要显示功能')}
         </Text>
       </div>
 
-        {sectionConfigs.map((section) => (
-          <div key={section.key} className='mb-6'>
-            {/* 区域标题和总开关 */}
-            <div className='flex justify-between items-center mb-4 p-4 bg-gray-50 rounded-xl border border-gray-200'>
-              <div>
-                <div className='font-semibold text-base text-gray-900 mb-1'>
-                  {section.title}
-                </div>
-                <Text className='text-xs text-gray-600'>
-                  {section.description}
-                </Text>
+      {sectionConfigs.map((section) => (
+        <div key={section.key} className='mb-6'>
+          {/* 区域标题和总开关 */}
+          <div className='flex justify-between items-center mb-4 p-4 bg-gray-50 rounded-xl border border-gray-200'>
+            <div>
+              <div className='font-semibold text-base text-gray-900 mb-1'>
+                {section.title}
               </div>
-              <Switch
-                checked={sidebarModulesUser[section.key]?.enabled}
-                onChange={handleSectionChange(section.key)}
-                size="default"
-              />
+              <Text className='text-xs text-gray-600'>
+                {section.description}
+              </Text>
             </div>
+            <Switch
+              checked={sidebarModulesUser[section.key]?.enabled}
+              onChange={handleSectionChange(section.key)}
+              size='default'
+            />
+          </div>
 
-            {/* 功能模块网格 */}
-            <Row gutter={[12, 12]}>
-              {section.modules.map((module) => (
-                <Col key={module.key} xs={24} sm={12} md={8} lg={6} xl={6}>
-                  <Card
-                    className={`!rounded-xl border border-gray-200 hover:border-blue-300 transition-all duration-200 ${
-                      sidebarModulesUser[section.key]?.enabled ? '' : 'opacity-50'
-                    }`}
-                    bodyStyle={{ padding: '16px' }}
-                    hoverable
-                  >
-                    <div className='flex justify-between items-center h-full'>
-                      <div className='flex-1 text-left'>
-                        <div className='font-semibold text-sm text-gray-900 mb-1'>
-                          {module.title}
-                        </div>
-                        <Text className='text-xs text-gray-600 leading-relaxed block'>
-                          {module.description}
-                        </Text>
-                      </div>
-                      <div className='ml-4'>
-                        <Switch
-                          checked={sidebarModulesUser[section.key]?.[module.key]}
-                          onChange={handleModuleChange(section.key, module.key)}
-                          size="default"
-                          disabled={!sidebarModulesUser[section.key]?.enabled}
-                        />
+          {/* 功能模块网格 */}
+          <Row gutter={[12, 12]}>
+            {section.modules.map((module) => (
+              <Col key={module.key} xs={24} sm={12} md={8} lg={6} xl={6}>
+                <Card
+                  className={`!rounded-xl border border-gray-200 hover:border-blue-300 transition-all duration-200 ${
+                    sidebarModulesUser[section.key]?.enabled ? '' : 'opacity-50'
+                  }`}
+                  bodyStyle={{ padding: '16px' }}
+                  hoverable
+                >
+                  <div className='flex justify-between items-center h-full'>
+                    <div className='flex-1 text-left'>
+                      <div className='font-semibold text-sm text-gray-900 mb-1'>
+                        {module.title}
                       </div>
+                      <Text className='text-xs text-gray-600 leading-relaxed block'>
+                        {module.description}
+                      </Text>
                     </div>
-                  </Card>
-                </Col>
-              ))}
-            </Row>
-          </div>
-        ))}
+                    <div className='ml-4'>
+                      <Switch
+                        checked={sidebarModulesUser[section.key]?.[module.key]}
+                        onChange={handleModuleChange(section.key, module.key)}
+                        size='default'
+                        disabled={!sidebarModulesUser[section.key]?.enabled}
+                      />
+                    </div>
+                  </div>
+                </Card>
+              </Col>
+            ))}
+          </Row>
+        </div>
+      ))}
 
       {/* 底部按钮 */}
       <div className='flex justify-end gap-3 mt-6 pt-4 border-t border-gray-200'>

+ 1 - 3
web/src/pages/Setting/Ratio/ModelRationNotSetEditor.jsx

@@ -130,9 +130,7 @@ export default function ModelRatioNotSetEditor(props) {
 
   // 在 return 语句之前,先处理过滤和分页逻辑
   const filteredModels = models.filter((model) =>
-    searchText
-      ? model.name.includes(searchText)
-      : true,
+    searchText ? model.name.includes(searchText) : true,
   );
 
   // 然后基于过滤后的数据计算分页数据

+ 1 - 3
web/src/pages/Setting/Ratio/ModelSettingsVisualEditor.jsx

@@ -99,9 +99,7 @@ export default function ModelSettingsVisualEditor(props) {
 
   // 在 return 语句之前,先处理过滤和分页逻辑
   const filteredModels = models.filter((model) => {
-    const keywordMatch = searchText
-      ? model.name.includes(searchText)
-      : true;
+    const keywordMatch = searchText ? model.name.includes(searchText) : true;
     const conflictMatch = conflictOnly ? model.hasConflict : true;
     return keywordMatch && conflictMatch;
   });

+ 11 - 2
web/src/pages/Setting/Ratio/UpstreamRatioSync.jsx

@@ -151,8 +151,17 @@ export default function UpstreamRatioSync(props) {
         setChannelEndpoints((prev) => {
           const merged = { ...prev };
           transferData.forEach((channel) => {
-            if (!merged[channel.key]) {
-              merged[channel.key] = DEFAULT_ENDPOINT;
+            const id = channel.key;
+            const base = channel._originalData?.base_url || '';
+            const name = channel.label || '';
+            const isOfficial =
+              id === -100 ||
+              base === 'https://basellm.github.io' ||
+              name === '官方倍率预设';
+            if (!merged[id]) {
+              merged[id] = isOfficial
+                ? '/llm-metadata/api/newapi/ratio_config-v1-base.json'
+                : DEFAULT_ENDPOINT;
             }
           });
           return merged;