✨ 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
This commit is contained in:
@@ -64,7 +64,7 @@ export const useHeaderBar = ({ onMobileMenuToggle, drawerOpen }) => {
|
||||
if (typeof modules.pricing === 'boolean') {
|
||||
modules.pricing = {
|
||||
enabled: modules.pricing,
|
||||
requireAuth: false // 默认不需要登录鉴权
|
||||
requireAuth: false, // 默认不需要登录鉴权
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user