mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-04-19 03:07:27 +00:00
Merge branch 'main-upstream' into feature/passkey
# Conflicts: # web/src/components/settings/PersonalSetting.jsx # web/src/i18n/locales/en.json # web/src/i18n/locales/zh.json
This commit is contained in:
@@ -58,7 +58,7 @@ const SiderBar = ({ onNavigate = () => {} }) => {
|
||||
loading: sidebarLoading,
|
||||
} = useSidebar();
|
||||
|
||||
const showSkeleton = useMinimumLoadingTime(sidebarLoading);
|
||||
const showSkeleton = useMinimumLoadingTime(sidebarLoading, 200);
|
||||
|
||||
const [selectedKeys, setSelectedKeys] = useState(['home']);
|
||||
const [chatItems, setChatItems] = useState([]);
|
||||
|
||||
@@ -20,7 +20,7 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
import React from 'react';
|
||||
import { Button, Dropdown } from '@douyinfe/semi-ui';
|
||||
import { Languages } from 'lucide-react';
|
||||
import { CN, GB } from 'country-flag-icons/react/3x2';
|
||||
import { CN, GB, FR } from 'country-flag-icons/react/3x2';
|
||||
|
||||
const LanguageSelector = ({ currentLang, onLanguageChange, t }) => {
|
||||
return (
|
||||
@@ -42,12 +42,19 @@ const LanguageSelector = ({ currentLang, onLanguageChange, t }) => {
|
||||
<GB title='English' className='!w-5 !h-auto' />
|
||||
<span>English</span>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
onClick={() => onLanguageChange('fr')}
|
||||
className={`!flex !items-center !gap-2 !px-3 !py-1.5 !text-sm !text-semi-color-text-0 dark:!text-gray-200 ${currentLang === 'fr' ? '!bg-semi-color-primary-light-default dark:!bg-blue-600 !font-semibold' : 'hover:!bg-semi-color-fill-1 dark:hover:!bg-gray-600'}`}
|
||||
>
|
||||
<FR title='Français' className='!w-5 !h-auto' />
|
||||
<span>Français</span>
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
icon={<Languages size={18} />}
|
||||
aria-label={t('切换语言')}
|
||||
aria-label={t('common.changeLanguage')}
|
||||
theme='borderless'
|
||||
type='tertiary'
|
||||
className='!p-1.5 !text-current focus:!bg-semi-color-fill-1 dark:focus:!bg-gray-700 !rounded-full !bg-semi-color-fill-0 dark:!bg-semi-color-fill-1 hover:!bg-semi-color-fill-1 dark:hover:!bg-semi-color-fill-2'
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
prepareCredentialCreationOptions,
|
||||
buildRegistrationResult,
|
||||
isPasskeySupported,
|
||||
setUserData,
|
||||
} from '../../helpers';
|
||||
import { UserContext } from '../../context/User';
|
||||
import { Modal } from '@douyinfe/semi-ui';
|
||||
@@ -253,6 +254,7 @@ const PersonalSetting = () => {
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
userDispatch({ type: 'login', payload: data });
|
||||
setUserData(data);
|
||||
await loadPasskeyStatus();
|
||||
} else {
|
||||
showError(message);
|
||||
|
||||
@@ -105,6 +105,7 @@ const MODEL_FETCHABLE_TYPES = new Set([
|
||||
40,
|
||||
42,
|
||||
48,
|
||||
43,
|
||||
]);
|
||||
|
||||
function type2secretPrompt(type) {
|
||||
|
||||
@@ -118,6 +118,9 @@ const EditTagModal = (props) => {
|
||||
case 36:
|
||||
localModels = ['suno_music', 'suno_lyrics'];
|
||||
break;
|
||||
case 53:
|
||||
localModels = ['NousResearch/Hermes-4-405B-FP8', 'Qwen/Qwen3-235B-A22B-Thinking-2507', 'Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8','Qwen/Qwen3-235B-A22B-Instruct-2507', 'zai-org/GLM-4.5-FP8', 'openai/gpt-oss-120b', 'deepseek-ai/DeepSeek-R1-0528', 'deepseek-ai/DeepSeek-R1', 'deepseek-ai/DeepSeek-V3-0324', 'deepseek-ai/DeepSeek-V3.1'];
|
||||
break;
|
||||
default:
|
||||
localModels = getChannelModels(value);
|
||||
break;
|
||||
|
||||
@@ -159,6 +159,11 @@ export const CHANNEL_OPTIONS = [
|
||||
color: 'purple',
|
||||
label: 'Vidu',
|
||||
},
|
||||
{
|
||||
value: 53,
|
||||
color: 'blue',
|
||||
label: 'SubModel',
|
||||
},
|
||||
];
|
||||
|
||||
export const MODEL_TABLE_PAGE_SIZE = 10;
|
||||
|
||||
@@ -1200,25 +1200,25 @@ export function renderModelPrice(
|
||||
const extraServices = [
|
||||
webSearch && webSearchCallCount > 0
|
||||
? i18next.t(
|
||||
' + Web搜索 {{count}}次 / 1K 次 * ${{price}} * {{ratioType}} {{ratio}}',
|
||||
{
|
||||
count: webSearchCallCount,
|
||||
price: webSearchPrice,
|
||||
ratio: groupRatio,
|
||||
ratioType: ratioLabel,
|
||||
},
|
||||
)
|
||||
' + Web搜索 {{count}}次 / 1K 次 * ${{price}} * {{ratioType}} {{ratio}}',
|
||||
{
|
||||
count: webSearchCallCount,
|
||||
price: webSearchPrice,
|
||||
ratio: groupRatio,
|
||||
ratioType: ratioLabel,
|
||||
},
|
||||
)
|
||||
: '',
|
||||
fileSearch && fileSearchCallCount > 0
|
||||
? i18next.t(
|
||||
' + 文件搜索 {{count}}次 / 1K 次 * ${{price}} * {{ratioType}} {{ratio}}',
|
||||
{
|
||||
count: fileSearchCallCount,
|
||||
price: fileSearchPrice,
|
||||
ratio: groupRatio,
|
||||
ratioType: ratioLabel,
|
||||
},
|
||||
)
|
||||
' + 文件搜索 {{count}}次 / 1K 次 * ${{price}} * {{ratioType}} {{ratio}}',
|
||||
{
|
||||
count: fileSearchCallCount,
|
||||
price: fileSearchPrice,
|
||||
ratio: groupRatio,
|
||||
ratioType: ratioLabel,
|
||||
},
|
||||
)
|
||||
: '',
|
||||
imageGenerationCall && imageGenerationCallPrice > 0
|
||||
? i18next.t(
|
||||
@@ -1398,10 +1398,10 @@ export function renderAudioModelPrice(
|
||||
let audioPrice =
|
||||
(audioInputTokens / 1000000) * inputRatioPrice * audioRatio * groupRatio +
|
||||
(audioCompletionTokens / 1000000) *
|
||||
inputRatioPrice *
|
||||
audioRatio *
|
||||
audioCompletionRatio *
|
||||
groupRatio;
|
||||
inputRatioPrice *
|
||||
audioRatio *
|
||||
audioCompletionRatio *
|
||||
groupRatio;
|
||||
let price = textPrice + audioPrice;
|
||||
return (
|
||||
<>
|
||||
@@ -1457,27 +1457,27 @@ export function renderAudioModelPrice(
|
||||
<p>
|
||||
{cacheTokens > 0
|
||||
? i18next.t(
|
||||
'文字提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}',
|
||||
{
|
||||
nonCacheInput: inputTokens - cacheTokens,
|
||||
cacheInput: cacheTokens,
|
||||
cachePrice: inputRatioPrice * cacheRatio,
|
||||
price: inputRatioPrice,
|
||||
completion: completionTokens,
|
||||
compPrice: completionRatioPrice,
|
||||
total: textPrice.toFixed(6),
|
||||
},
|
||||
)
|
||||
'文字提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}',
|
||||
{
|
||||
nonCacheInput: inputTokens - cacheTokens,
|
||||
cacheInput: cacheTokens,
|
||||
cachePrice: inputRatioPrice * cacheRatio,
|
||||
price: inputRatioPrice,
|
||||
completion: completionTokens,
|
||||
compPrice: completionRatioPrice,
|
||||
total: textPrice.toFixed(6),
|
||||
},
|
||||
)
|
||||
: i18next.t(
|
||||
'文字提示 {{input}} tokens / 1M tokens * ${{price}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}',
|
||||
{
|
||||
input: inputTokens,
|
||||
price: inputRatioPrice,
|
||||
completion: completionTokens,
|
||||
compPrice: completionRatioPrice,
|
||||
total: textPrice.toFixed(6),
|
||||
},
|
||||
)}
|
||||
'文字提示 {{input}} tokens / 1M tokens * ${{price}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}',
|
||||
{
|
||||
input: inputTokens,
|
||||
price: inputRatioPrice,
|
||||
completion: completionTokens,
|
||||
compPrice: completionRatioPrice,
|
||||
total: textPrice.toFixed(6),
|
||||
},
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{i18next.t(
|
||||
@@ -1617,35 +1617,35 @@ export function renderClaudeModelPrice(
|
||||
<p>
|
||||
{cacheTokens > 0 || cacheCreationTokens > 0
|
||||
? i18next.t(
|
||||
'提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 缓存创建 {{cacheCreationInput}} tokens / 1M tokens * ${{cacheCreationPrice}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * {{ratioType}} {{ratio}} = ${{total}}',
|
||||
{
|
||||
nonCacheInput: nonCachedTokens,
|
||||
cacheInput: cacheTokens,
|
||||
cacheRatio: cacheRatio,
|
||||
cacheCreationInput: cacheCreationTokens,
|
||||
cacheCreationRatio: cacheCreationRatio,
|
||||
cachePrice: cacheRatioPrice,
|
||||
cacheCreationPrice: cacheCreationRatioPrice,
|
||||
price: inputRatioPrice,
|
||||
completion: completionTokens,
|
||||
compPrice: completionRatioPrice,
|
||||
ratio: groupRatio,
|
||||
ratioType: ratioLabel,
|
||||
total: price.toFixed(6),
|
||||
},
|
||||
)
|
||||
'提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 缓存创建 {{cacheCreationInput}} tokens / 1M tokens * ${{cacheCreationPrice}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * {{ratioType}} {{ratio}} = ${{total}}',
|
||||
{
|
||||
nonCacheInput: nonCachedTokens,
|
||||
cacheInput: cacheTokens,
|
||||
cacheRatio: cacheRatio,
|
||||
cacheCreationInput: cacheCreationTokens,
|
||||
cacheCreationRatio: cacheCreationRatio,
|
||||
cachePrice: cacheRatioPrice,
|
||||
cacheCreationPrice: cacheCreationRatioPrice,
|
||||
price: inputRatioPrice,
|
||||
completion: completionTokens,
|
||||
compPrice: completionRatioPrice,
|
||||
ratio: groupRatio,
|
||||
ratioType: ratioLabel,
|
||||
total: price.toFixed(6),
|
||||
},
|
||||
)
|
||||
: i18next.t(
|
||||
'提示 {{input}} tokens / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * {{ratioType}} {{ratio}} = ${{total}}',
|
||||
{
|
||||
input: inputTokens,
|
||||
price: inputRatioPrice,
|
||||
completion: completionTokens,
|
||||
compPrice: completionRatioPrice,
|
||||
ratio: groupRatio,
|
||||
ratioType: ratioLabel,
|
||||
total: price.toFixed(6),
|
||||
},
|
||||
)}
|
||||
'提示 {{input}} tokens / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * {{ratioType}} {{ratio}} = ${{total}}',
|
||||
{
|
||||
input: inputTokens,
|
||||
price: inputRatioPrice,
|
||||
completion: completionTokens,
|
||||
compPrice: completionRatioPrice,
|
||||
ratio: groupRatio,
|
||||
ratioType: ratioLabel,
|
||||
total: price.toFixed(6),
|
||||
},
|
||||
)}
|
||||
</p>
|
||||
<p>{i18next.t('仅供参考,以实际扣费为准')}</p>
|
||||
</article>
|
||||
|
||||
@@ -40,7 +40,7 @@ export const useHeaderBar = ({ onMobileMenuToggle, drawerOpen }) => {
|
||||
const location = useLocation();
|
||||
|
||||
const loading = statusState?.status === undefined;
|
||||
const isLoading = useMinimumLoadingTime(loading);
|
||||
const isLoading = useMinimumLoadingTime(loading, 200);
|
||||
|
||||
const systemName = getSystemName();
|
||||
const logo = getLogo();
|
||||
|
||||
@@ -17,7 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useMemo, useContext } from 'react';
|
||||
import { useState, useEffect, useMemo, useContext, useRef } from 'react';
|
||||
import { StatusContext } from '../../context/Status';
|
||||
import { API } from '../../helpers';
|
||||
|
||||
@@ -29,6 +29,13 @@ export const useSidebar = () => {
|
||||
const [statusState] = useContext(StatusContext);
|
||||
const [userConfig, setUserConfig] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const instanceIdRef = useRef(null);
|
||||
const hasLoadedOnceRef = useRef(false);
|
||||
|
||||
if (!instanceIdRef.current) {
|
||||
const randomPart = Math.random().toString(16).slice(2);
|
||||
instanceIdRef.current = `sidebar-${Date.now()}-${randomPart}`;
|
||||
}
|
||||
|
||||
// 默认配置
|
||||
const defaultAdminConfig = {
|
||||
@@ -74,9 +81,17 @@ export const useSidebar = () => {
|
||||
}, [statusState?.status?.SidebarModulesAdmin]);
|
||||
|
||||
// 加载用户配置的通用方法
|
||||
const loadUserConfig = async () => {
|
||||
const loadUserConfig = async ({ withLoading } = {}) => {
|
||||
const shouldShowLoader =
|
||||
typeof withLoading === 'boolean'
|
||||
? withLoading
|
||||
: !hasLoadedOnceRef.current;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
if (shouldShowLoader) {
|
||||
setLoading(true);
|
||||
}
|
||||
|
||||
const res = await API.get('/api/user/self');
|
||||
if (res.data.success && res.data.data.sidebar_modules) {
|
||||
let config;
|
||||
@@ -122,18 +137,25 @@ export const useSidebar = () => {
|
||||
});
|
||||
setUserConfig(defaultUserConfig);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
if (shouldShowLoader) {
|
||||
setLoading(false);
|
||||
}
|
||||
hasLoadedOnceRef.current = true;
|
||||
}
|
||||
};
|
||||
|
||||
// 刷新用户配置的方法(供外部调用)
|
||||
const refreshUserConfig = async () => {
|
||||
if (Object.keys(adminConfig).length > 0) {
|
||||
await loadUserConfig();
|
||||
if (Object.keys(adminConfig).length > 0) {
|
||||
await loadUserConfig({ withLoading: false });
|
||||
}
|
||||
|
||||
// 触发全局刷新事件,通知所有useSidebar实例更新
|
||||
sidebarEventTarget.dispatchEvent(new CustomEvent(SIDEBAR_REFRESH_EVENT));
|
||||
sidebarEventTarget.dispatchEvent(
|
||||
new CustomEvent(SIDEBAR_REFRESH_EVENT, {
|
||||
detail: { sourceId: instanceIdRef.current, skipLoader: true },
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
// 加载用户配置
|
||||
@@ -146,9 +168,15 @@ export const useSidebar = () => {
|
||||
|
||||
// 监听全局刷新事件
|
||||
useEffect(() => {
|
||||
const handleRefresh = () => {
|
||||
const handleRefresh = (event) => {
|
||||
if (event?.detail?.sourceId === instanceIdRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Object.keys(adminConfig).length > 0) {
|
||||
loadUserConfig();
|
||||
loadUserConfig({
|
||||
withLoading: event?.detail?.skipLoader ? false : undefined,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import { initReactI18next } from 'react-i18next';
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
|
||||
import enTranslation from './locales/en.json';
|
||||
import frTranslation from './locales/fr.json';
|
||||
import zhTranslation from './locales/zh.json';
|
||||
|
||||
i18n
|
||||
@@ -36,6 +37,9 @@ i18n
|
||||
zh: {
|
||||
translation: zhTranslation,
|
||||
},
|
||||
fr: {
|
||||
translation: frTranslation,
|
||||
},
|
||||
},
|
||||
fallbackLng: 'zh',
|
||||
interpolation: {
|
||||
|
||||
@@ -1406,6 +1406,7 @@
|
||||
"Claude思考适配 BudgetTokens = MaxTokens * BudgetTokens 百分比": "Claude thinking adaptation BudgetTokens = MaxTokens * BudgetTokens percentage",
|
||||
"思考适配 BudgetTokens 百分比": "Thinking adaptation BudgetTokens percentage",
|
||||
"0.1-1之间的小数": "Decimal between 0.1 and 1",
|
||||
"0.1以上的小数": "Decimal above 0.1",
|
||||
"模型相关设置": "Model related settings",
|
||||
"收起侧边栏": "Collapse sidebar",
|
||||
"展开侧边栏": "Expand sidebar",
|
||||
@@ -2185,5 +2186,9 @@
|
||||
"允许的 Origins": "Allowed Origins",
|
||||
"留空将自动使用服务器地址,多个 Origin 用于支持多域名部署": "Leave blank to auto-use server address, multiple Origins for multi-domain deployment",
|
||||
"输入 Origin 后回车,如:https://example.com": "Enter Origin and press Enter, e.g.: https://example.com",
|
||||
"保存 Passkey 设置": "Save Passkey Settings"
|
||||
"保存 Passkey 设置": "Save Passkey Settings",
|
||||
"黑名单": "Blacklist",
|
||||
"common": {
|
||||
"changeLanguage": "Change Language"
|
||||
}
|
||||
}
|
||||
|
||||
2141
web/src/i18n/locales/fr.json
Normal file
2141
web/src/i18n/locales/fr.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -34,6 +34,9 @@
|
||||
"输入端口后回车,如:80 或 8000-8999": "输入端口后回车,如:80 或 8000-8999",
|
||||
"更新SSRF防护设置": "更新SSRF防护设置",
|
||||
"域名IP过滤详细说明": "⚠️此功能为实验性选项,域名可能解析到多个 IPv4/IPv6 地址,若开启,请确保 IP 过滤列表覆盖这些地址,否则可能导致访问失败。",
|
||||
"common": {
|
||||
"changeLanguage": "切换语言"
|
||||
},
|
||||
"允许在 Stripe 支付中输入促销码": "允许在 Stripe 支付中输入促销码",
|
||||
"Passkey 认证": "Passkey 认证",
|
||||
"已启用 Passkey,可进行无密码认证": "已启用 Passkey,可进行无密码认证",
|
||||
|
||||
@@ -202,9 +202,8 @@ export default function SettingClaudeModel(props) {
|
||||
label={t('思考适配 BudgetTokens 百分比')}
|
||||
field={'claude.thinking_adapter_budget_tokens_percentage'}
|
||||
initValue={''}
|
||||
extraText={t('0.1-1之间的小数')}
|
||||
extraText={t('0.1以上的小数')}
|
||||
min={0.1}
|
||||
max={1}
|
||||
onChange={(value) =>
|
||||
setInputs({
|
||||
...inputs,
|
||||
|
||||
Reference in New Issue
Block a user