fix(default): resolve v1 frontend issue regressions

Fix v1 frontend regressions across channel forms, dashboard charts, wallet history, payment callbacks, invite links, API key groups, rate-limit errors, and usage-log scrolling.

Fixes #4715
Fixes #4618
Fixes #4699
Fixes #4651
Fixes #4637
Fixes #4682
Fixes #4691
Fixes #4565
Fixes #4334
This commit is contained in:
CaIon 2026-05-11 11:25:05 +08:00
parent 5fa103fa5b
commit ba474393fb
No known key found for this signature in database
GPG Key ID: 0CFA613529A9921D
27 changed files with 267 additions and 41 deletions

View File

@ -401,9 +401,6 @@ func GetChannelById(id int, selectAll bool) (*Channel, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
if channel == nil {
return nil, errors.New("channel not found")
}
return channel, nil return channel, nil
} }
@ -758,7 +755,7 @@ func EditChannelByTag(tag string, newTag *string, modelMapping *string, models *
updateData.Tag = newTag updateData.Tag = newTag
updatedTag = *newTag updatedTag = *newTag
} }
if modelMapping != nil && *modelMapping != "" { if modelMapping != nil {
updateData.ModelMapping = modelMapping updateData.ModelMapping = modelMapping
} }
if models != nil && *models != "" { if models != nil && *models != "" {

View File

@ -36,6 +36,7 @@ interface ComboboxInputProps {
emptyText?: string emptyText?: string
className?: string className?: string
id?: string id?: string
allowCustomValue?: boolean
} }
export function ComboboxInput({ export function ComboboxInput({
@ -46,23 +47,30 @@ export function ComboboxInput({
emptyText = 'No option found.', emptyText = 'No option found.',
className, className,
id, id,
allowCustomValue = false,
}: ComboboxInputProps) { }: ComboboxInputProps) {
const { t } = useTranslation() const { t } = useTranslation()
const [open, setOpen] = React.useState(false) const [open, setOpen] = React.useState(false)
const [searchValue, setSearchValue] = React.useState('')
const [highlightedIndex, setHighlightedIndex] = React.useState(-1) const [highlightedIndex, setHighlightedIndex] = React.useState(-1)
const containerRef = React.useRef<HTMLDivElement>(null) const containerRef = React.useRef<HTMLDivElement>(null)
const inputRef = React.useRef<HTMLInputElement>(null) const inputRef = React.useRef<HTMLInputElement>(null)
const listRef = React.useRef<HTMLUListElement>(null) const listRef = React.useRef<HTMLUListElement>(null)
const selectedOption = React.useMemo(
() => options.find((option) => option.value === value),
[options, value]
)
const displayValue = open ? searchValue : (selectedOption?.label ?? value)
const filteredOptions = React.useMemo(() => { const filteredOptions = React.useMemo(() => {
if (!value.trim()) return options if (!searchValue.trim()) return options
const search = value.toLowerCase().trim() const search = searchValue.toLowerCase().trim()
return options.filter( return options.filter(
(option) => (option) =>
option.label.toLowerCase().includes(search) || option.label.toLowerCase().includes(search) ||
option.value.toLowerCase().includes(search) option.value.toLowerCase().includes(search)
) )
}, [options, value]) }, [options, searchValue])
// Reset highlight when filtered options change // Reset highlight when filtered options change
React.useEffect(() => { React.useEffect(() => {
@ -79,6 +87,7 @@ export function ComboboxInput({
!containerRef.current.contains(e.target as Node) !containerRef.current.contains(e.target as Node)
) { ) {
setOpen(false) setOpen(false)
setSearchValue('')
} }
} }
@ -89,6 +98,7 @@ export function ComboboxInput({
const handleSelect = (selectedValue: string) => { const handleSelect = (selectedValue: string) => {
onValueChange(selectedValue) onValueChange(selectedValue)
setOpen(false) setOpen(false)
setSearchValue('')
inputRef.current?.focus() inputRef.current?.focus()
} }
@ -117,14 +127,18 @@ export function ComboboxInput({
e.preventDefault() e.preventDefault()
if (highlightedIndex >= 0 && filteredOptions[highlightedIndex]) { if (highlightedIndex >= 0 && filteredOptions[highlightedIndex]) {
handleSelect(filteredOptions[highlightedIndex].value) handleSelect(filteredOptions[highlightedIndex].value)
} else if (allowCustomValue && searchValue.trim()) {
handleSelect(searchValue.trim())
} else { } else {
// No highlighted option, just close the dropdown and keep current value // No highlighted option, just close the dropdown and keep current value
setOpen(false) setOpen(false)
setSearchValue('')
} }
break break
case 'Escape': case 'Escape':
e.preventDefault() e.preventDefault()
setOpen(false) setOpen(false)
setSearchValue('')
break break
} }
} }
@ -136,7 +150,9 @@ export function ComboboxInput({
item?.scrollIntoView({ block: 'nearest' }) item?.scrollIntoView({ block: 'nearest' })
}, [highlightedIndex]) }, [highlightedIndex])
const showDropdown = open && (filteredOptions.length > 0 || value.trim()) const showDropdown =
open &&
(filteredOptions.length > 0 || (allowCustomValue && searchValue.trim()))
return ( return (
<div ref={containerRef} className='relative'> <div ref={containerRef} className='relative'>
@ -150,12 +166,19 @@ export function ComboboxInput({
aria-autocomplete='list' aria-autocomplete='list'
autoComplete='off' autoComplete='off'
placeholder={placeholder} placeholder={placeholder}
value={value} value={displayValue}
onChange={(e) => { onChange={(e) => {
onValueChange(e.target.value) const nextValue = e.target.value
setSearchValue(nextValue)
if (allowCustomValue) {
onValueChange(nextValue)
}
if (!open) setOpen(true) if (!open) setOpen(true)
}} }}
onFocus={() => setOpen(true)} onFocus={() => {
setSearchValue(allowCustomValue ? value : '')
setOpen(true)
}}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
className={cn('pr-9', className)} className={cn('pr-9', className)}
/> />
@ -200,10 +223,12 @@ export function ComboboxInput({
</ul> </ul>
) : ( ) : (
<div className='px-2 py-6 text-center text-sm'> <div className='px-2 py-6 text-center text-sm'>
{emptyText} {t(emptyText)}
{value.trim() && ( {allowCustomValue && searchValue.trim() && (
<div className='text-muted-foreground mt-1 text-xs'> <div className='text-muted-foreground mt-1 text-xs'>
{t('Press Enter to use "{{value}}"', { value: value.trim() })} {t('Press Enter to use "{{value}}"', {
value: searchValue.trim(),
})}
</div> </div>
)} )}
</div> </div>

View File

@ -68,6 +68,7 @@ function Combobox(
placeholder={props.searchPlaceholder ?? props.placeholder} placeholder={props.searchPlaceholder ?? props.placeholder}
emptyText={props.emptyText} emptyText={props.emptyText}
className={props.className} className={props.className}
allowCustomValue={props.allowCustomValue}
/> />
) )
} }

View File

@ -484,6 +484,12 @@ export function transformFormDataToUpdatePayload(
} }
}) })
// Send explicit empty strings for nullable JSON/text fields so GORM updates can clear them.
payload.model_mapping = formData.model_mapping || ''
payload.status_code_mapping = formData.status_code_mapping || ''
payload.param_override = formData.param_override || ''
payload.header_override = formData.header_override || ''
return payload return payload
} }

View File

@ -143,6 +143,12 @@ function replaceToken(source: string, token: string, value: string) {
return source.split(token).join(value) return source.split(token).join(value)
} }
function normalizeApiKey(apiKey: string): string {
const trimmed = apiKey.trim()
if (!trimmed) return ''
return trimmed.startsWith('sk-') ? trimmed : `sk-${trimmed}`
}
export function resolveChatUrl({ export function resolveChatUrl({
template, template,
apiKey, apiKey,
@ -151,7 +157,7 @@ export function resolveChatUrl({
let url = template let url = template
const safeServerAddress = serverAddress || '' const safeServerAddress = serverAddress || ''
const safeApiKey = apiKey || '' const safeApiKey = normalizeApiKey(apiKey || '')
if (url.includes('{cherryConfig}')) { if (url.includes('{cherryConfig}')) {
const payload = { const payload = {

View File

@ -115,6 +115,15 @@ export function ConsumptionDistributionChart(
] ]
) )
const spec = chartType === 'bar' ? chartData.spec_line : chartData.spec_area const spec = chartType === 'bar' ? chartData.spec_line : chartData.spec_area
const specType = typeof spec?.type === 'string' ? spec.type : chartType
const chartKey = [
chartType,
specType,
props.loading ? 'loading' : 'ready',
props.data.length,
resolvedTheme,
customization.preset,
].join('-')
return ( return (
<div className='overflow-hidden rounded-lg border'> <div className='overflow-hidden rounded-lg border'>
@ -152,7 +161,7 @@ export function ConsumptionDistributionChart(
<div className='h-[300px] p-1.5 sm:h-96 sm:p-2'> <div className='h-[300px] p-1.5 sm:h-96 sm:p-2'>
{themeReady && spec && ( {themeReady && spec && (
<VChart <VChart
key={`${chartType}-${resolvedTheme}-${customization.preset}`} key={chartKey}
spec={{ spec={{
...spec, ...spec,
theme: resolvedTheme === 'dark' ? 'dark' : 'light', theme: resolvedTheme === 'dark' ? 'dark' : 'light',

View File

@ -114,6 +114,15 @@ export function ModelCharts(props: ModelChartsProps) {
) )
const spec = chartData[CHART_SPEC_KEYS[activeTab]] const spec = chartData[CHART_SPEC_KEYS[activeTab]]
const specType = typeof spec?.type === 'string' ? spec.type : activeTab
const chartKey = [
activeTab,
specType,
props.loading ? 'loading' : 'ready',
props.data.length,
resolvedTheme,
customization.preset,
].join('-')
return ( return (
<div className='overflow-hidden rounded-lg border'> <div className='overflow-hidden rounded-lg border'>
@ -149,7 +158,7 @@ export function ModelCharts(props: ModelChartsProps) {
<div className='h-[300px] p-1.5 sm:h-96 sm:p-2'> <div className='h-[300px] p-1.5 sm:h-96 sm:p-2'>
{themeReady && spec && ( {themeReady && spec && (
<VChart <VChart
key={`${activeTab}-${resolvedTheme}-${customization.preset}`} key={chartKey}
spec={{ spec={{
...spec, ...spec,
theme: resolvedTheme === 'dark' ? 'dark' : 'light', theme: resolvedTheme === 'dark' ? 'dark' : 'light',

View File

@ -212,7 +212,7 @@ export function processChartData(
legends: { visible: true, selectMode: 'single' }, legends: { visible: true, selectMode: 'single' },
}, },
spec_model_line: { spec_model_line: {
type: 'line', type: 'area',
data: [{ id: 'lineData', values: [] }], data: [{ id: 'lineData', values: [] }],
xField: 'Time', xField: 'Time',
yField: 'Count', yField: 'Count',

View File

@ -25,27 +25,45 @@ const FEEDBACK_URL = 'https://github.com/QuantumNous/new-api/issues'
type GeneralErrorProps = React.HTMLAttributes<HTMLDivElement> & { type GeneralErrorProps = React.HTMLAttributes<HTMLDivElement> & {
minimal?: boolean minimal?: boolean
error?: unknown
}
function getHttpStatus(error: unknown): number | undefined {
if (typeof error !== 'object' || error === null) return undefined
const response = (error as Record<string, unknown>).response
if (typeof response !== 'object' || response === null) return undefined
const status = (response as Record<string, unknown>).status
return typeof status === 'number' ? status : undefined
} }
export function GeneralError({ export function GeneralError({
className, className,
minimal = false, minimal = false,
error,
}: GeneralErrorProps) { }: GeneralErrorProps) {
const { t } = useTranslation() const { t } = useTranslation()
const navigate = useNavigate() const navigate = useNavigate()
const { history } = useRouter() const { history } = useRouter()
const status = getHttpStatus(error)
const isRateLimited = status === 429
const title = isRateLimited
? t('Too many requests')
: `${t('Oops! Something went wrong')} ${`:')`}`
const description = isRateLimited
? t('Please wait a moment before trying again.')
: t('Please try again later.')
return ( return (
<div className={cn('h-svh w-full', className)}> <div className={cn('h-svh w-full', className)}>
<div className='m-auto flex h-full w-full flex-col items-center justify-center gap-2'> <div className='m-auto flex h-full w-full flex-col items-center justify-center gap-2'>
{!minimal && ( {!minimal && (
<h1 className='text-[7rem] leading-tight font-bold'>500</h1> <h1 className='text-[7rem] leading-tight font-bold'>
{status ?? 500}
</h1>
)} )}
<span className='font-medium'> <span className='font-medium'>{title}</span>
{t('Oops! Something went wrong')} {`:')`}
</span>
<p className='text-muted-foreground text-center'> <p className='text-muted-foreground text-center'>
{t('We apologize for the inconvenience.')} <br />{' '} {t('We apologize for the inconvenience.')} <br /> {description}
{t('Please try again later.')}
</p> </p>
{!minimal && ( {!minimal && (
<p className='text-muted-foreground text-center text-sm'> <p className='text-muted-foreground text-center text-sm'>

View File

@ -145,7 +145,7 @@ export function ApiKeyGroupCombobox({
<span className='flex min-w-0 flex-1 items-center justify-between gap-2 sm:gap-3'> <span className='flex min-w-0 flex-1 items-center justify-between gap-2 sm:gap-3'>
<span className='min-w-0'> <span className='min-w-0'>
<span className='block truncate font-medium'> <span className='block truncate font-medium'>
{selectedOption?.value || placeholder || t('Select a group')} {selectedOption?.label || placeholder || t('Select a group')}
</span> </span>
{selectedOption?.desc && ( {selectedOption?.desc && (
<span className='text-muted-foreground block truncate text-[11px] sm:text-xs'> <span className='text-muted-foreground block truncate text-[11px] sm:text-xs'>
@ -178,7 +178,7 @@ export function ApiKeyGroupCombobox({
<CommandItem <CommandItem
key={option.value} key={option.value}
value={option.value} value={option.value}
onSelect={handleSelect} onSelect={() => handleSelect(option.value)}
className='data-[selected=true]:bg-muted items-start gap-3 rounded-lg px-3 py-3 transition-colors' className='data-[selected=true]:bg-muted items-start gap-3 rounded-lg px-3 py-3 transition-colors'
> >
<Check <Check
@ -189,7 +189,7 @@ export function ApiKeyGroupCombobox({
/> />
<span className='min-w-0 flex-1'> <span className='min-w-0 flex-1'>
<span className='block truncate font-medium'> <span className='block truncate font-medium'>
{option.value} {option.label}
</span> </span>
{option.desc && ( {option.desc && (
<span className='text-muted-foreground block truncate text-xs'> <span className='text-muted-foreground block truncate text-xs'>

View File

@ -126,7 +126,7 @@ export function RateLimitSection({ defaultValues }: RateLimitSectionProps) {
</FormLabel> </FormLabel>
<FormDescription> <FormDescription>
{t( {t(
'Restrict user model request frequency (may impact high concurrency performance)' 'This controls model request rate limiting. Web/API route throttling is configured by environment variables and may still return 429.'
)} )}
</FormDescription> </FormDescription>
</div> </div>

View File

@ -171,6 +171,7 @@ export function UsageLogsTable({ logCategory }: UsageLogsTableProps) {
'No usage logs available. Logs will appear here once API calls are made.' 'No usage logs available. Logs will appear here once API calls are made.'
)} )}
skeletonKeyPrefix='usage-log-skeleton' skeletonKeyPrefix='usage-log-skeleton'
tableClassName='max-h-[calc(100dvh-13rem)] overflow-auto sm:max-h-[calc(100dvh-14rem)]'
tableHeaderClassName='bg-muted/30 sticky top-0 z-10' tableHeaderClassName='bg-muted/30 sticky top-0 z-10'
toolbar={ toolbar={
isCommon ? ( isCommon ? (

View File

@ -176,8 +176,8 @@ export function BillingHistoryDialog({
</p> </p>
<p className='mt-1 text-xs'> <p className='mt-1 text-xs'>
{keyword {keyword
? 'Try adjusting your search' ? t('Try adjusting your search')
: 'Your transaction history will appear here'} : t('Your transaction history will appear here')}
</p> </p>
</div> </div>
) : ( ) : (
@ -233,15 +233,15 @@ export function BillingHistoryDialog({
<div className='mt-3 grid grid-cols-2 gap-3 sm:mt-4 sm:grid-cols-3 sm:gap-4'> <div className='mt-3 grid grid-cols-2 gap-3 sm:mt-4 sm:grid-cols-3 sm:gap-4'>
<div className='space-y-1'> <div className='space-y-1'>
<Label className='text-muted-foreground text-xs'> <Label className='text-muted-foreground text-xs'>
Payment Method {t('Payment Method')}
</Label> </Label>
<div className='text-sm font-medium'> <div className='text-sm font-medium'>
{getPaymentMethodName(record.payment_method)} {getPaymentMethodName(record.payment_method, t)}
</div> </div>
</div> </div>
<div className='space-y-1'> <div className='space-y-1'>
<Label className='text-muted-foreground text-xs'> <Label className='text-muted-foreground text-xs'>
Amount {t('Amount')}
</Label> </Label>
<div className='text-sm font-semibold'> <div className='text-sm font-semibold'>
{formatCurrencyFromUSD(record.amount, { {formatCurrencyFromUSD(record.amount, {
@ -253,7 +253,7 @@ export function BillingHistoryDialog({
</div> </div>
<div className='space-y-1'> <div className='space-y-1'>
<Label className='text-muted-foreground text-xs'> <Label className='text-muted-foreground text-xs'>
Payment {t('Payment')}
</Label> </Label>
<div className='text-sm font-semibold text-red-600'> <div className='text-sm font-semibold text-red-600'>
{formatNumber(record.money)} {formatNumber(record.money)}
@ -270,7 +270,7 @@ export function BillingHistoryDialog({
onClick={() => setConfirmTradeNo(record.trade_no)} onClick={() => setConfirmTradeNo(record.trade_no)}
disabled={completing} disabled={completing}
> >
Complete Order {t('Complete Order')}
</Button> </Button>
</div> </div>
)} )}
@ -341,7 +341,7 @@ export function BillingHistoryDialog({
onClick={handleConfirmComplete} onClick={handleConfirmComplete}
disabled={completing} disabled={completing}
> >
{completing ? 'Processing...' : 'Confirm'} {completing ? t('Processing...') : t('Confirm')}
</AlertDialogAction> </AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>

View File

@ -67,8 +67,12 @@ export const PAYMENT_METHOD_NAMES: Record<string, string> = {
/** /**
* Get payment method display name * Get payment method display name
*/ */
export function getPaymentMethodName(method: string): string { export function getPaymentMethodName(
return PAYMENT_METHOD_NAMES[method] || method method: string,
t?: (key: string) => string
): string {
const name = PAYMENT_METHOD_NAMES[method] || method
return t ? t(name) : name
} }
/** /**

View File

@ -17,13 +17,13 @@
"file": "ja.json", "file": "ja.json",
"missingCount": 0, "missingCount": 0,
"extrasCount": 0, "extrasCount": 0,
"untranslatedCount": 90 "untranslatedCount": 92
}, },
"ru": { "ru": {
"file": "ru.json", "file": "ru.json",
"missingCount": 0, "missingCount": 0,
"extrasCount": 0, "extrasCount": 0,
"untranslatedCount": 105 "untranslatedCount": 107
}, },
"vi": { "vi": {
"file": "vi.json", "file": "vi.json",

View File

@ -88,5 +88,7 @@
"Webhook URL:": "Webhook URL:", "Webhook URL:": "Webhook URL:",
"whsec_xxx": "whsec_xxx", "whsec_xxx": "whsec_xxx",
"Xinference": "Xinference", "Xinference": "Xinference",
"Xunfei": "Xunfei" "Xunfei": "Xunfei",
"Alipay": "Alipay",
"WeChat Pay": "WeChat Pay"
} }

View File

@ -103,5 +103,7 @@
"whsec_xxx": "whsec_xxx", "whsec_xxx": "whsec_xxx",
"Xinference": "Xinference", "Xinference": "Xinference",
"Xunfei": "Xunfei", "Xunfei": "Xunfei",
"Alipay": "Alipay",
"WeChat Pay": "WeChat Pay",
"Zhipu V4": "Zhipu V4" "Zhipu V4": "Zhipu V4"
} }

View File

@ -4400,6 +4400,13 @@
"Your Telegram Bot Token": "Your Telegram Bot Token", "Your Telegram Bot Token": "Your Telegram Bot Token",
"Your Turnstile secret key": "Your Turnstile secret key", "Your Turnstile secret key": "Your Turnstile secret key",
"Your Turnstile site key": "Your Turnstile site key", "Your Turnstile site key": "Your Turnstile site key",
"Alipay": "Alipay",
"Please wait a moment before trying again.": "Please wait a moment before trying again.",
"This controls model request rate limiting. Web/API route throttling is configured by environment variables and may still return 429.": "This controls model request rate limiting. Web/API route throttling is configured by environment variables and may still return 429.",
"Too many requests": "Too many requests",
"Try adjusting your search": "Try adjusting your search",
"WeChat Pay": "WeChat Pay",
"Your transaction history will appear here": "Your transaction history will appear here",
"Zero retention": "Zero retention", "Zero retention": "Zero retention",
"Zhipu": "Zhipu", "Zhipu": "Zhipu",
"Zhipu V4": "Zhipu V4", "Zhipu V4": "Zhipu V4",

View File

@ -4400,6 +4400,13 @@
"Your Telegram Bot Token": "Votre Jeton de Bot Telegram", "Your Telegram Bot Token": "Votre Jeton de Bot Telegram",
"Your Turnstile secret key": "Votre clé secrète Turnstile", "Your Turnstile secret key": "Votre clé secrète Turnstile",
"Your Turnstile site key": "Votre clé de site Turnstile", "Your Turnstile site key": "Votre clé de site Turnstile",
"Alipay": "Alipay",
"Please wait a moment before trying again.": "Veuillez patienter un instant avant de réessayer.",
"This controls model request rate limiting. Web/API route throttling is configured by environment variables and may still return 429.": "Ce réglage contrôle la limitation des requêtes de modèles. La limitation des routes Web/API se configure via les variables d'environnement et peut encore renvoyer 429.",
"Too many requests": "Trop de requêtes",
"Try adjusting your search": "Essayez d'ajuster votre recherche",
"WeChat Pay": "WeChat Pay",
"Your transaction history will appear here": "Votre historique de transactions apparaîtra ici",
"Zero retention": "Aucune rétention", "Zero retention": "Aucune rétention",
"Zhipu": "Zhipu", "Zhipu": "Zhipu",
"Zhipu V4": "Zhipu V4", "Zhipu V4": "Zhipu V4",

View File

@ -4400,6 +4400,13 @@
"Your Telegram Bot Token": "あなたのTelegramボットトークン", "Your Telegram Bot Token": "あなたのTelegramボットトークン",
"Your Turnstile secret key": "あなたのTurnstileシークレットキー", "Your Turnstile secret key": "あなたのTurnstileシークレットキー",
"Your Turnstile site key": "あなたのTurnstileサイトキー", "Your Turnstile site key": "あなたのTurnstileサイトキー",
"Alipay": "Alipay",
"Please wait a moment before trying again.": "しばらく待ってからもう一度お試しください。",
"This controls model request rate limiting. Web/API route throttling is configured by environment variables and may still return 429.": "これはモデルリクエストのレート制限を制御します。Web/API ルートのスロットリングは環境変数で設定され、引き続き 429 を返す場合があります。",
"Too many requests": "リクエストが多すぎます",
"Try adjusting your search": "検索条件を調整してみてください",
"WeChat Pay": "WeChat Pay",
"Your transaction history will appear here": "取引履歴はここに表示されます",
"Zero retention": "データ保持なし", "Zero retention": "データ保持なし",
"Zhipu": "Zhipu", "Zhipu": "Zhipu",
"Zhipu V4": "Zhipu V 4", "Zhipu V4": "Zhipu V 4",

View File

@ -4400,6 +4400,13 @@
"Your Telegram Bot Token": "Ваш токен Telegram-бота", "Your Telegram Bot Token": "Ваш токен Telegram-бота",
"Your Turnstile secret key": "Секретный ключ Turnstile", "Your Turnstile secret key": "Секретный ключ Turnstile",
"Your Turnstile site key": "Ключ сайта Turnstile", "Your Turnstile site key": "Ключ сайта Turnstile",
"Alipay": "Alipay",
"Please wait a moment before trying again.": "Пожалуйста, подождите немного и попробуйте снова.",
"This controls model request rate limiting. Web/API route throttling is configured by environment variables and may still return 429.": "Этот параметр управляет ограничением частоты запросов к моделям. Ограничение маршрутов Web/API настраивается переменными окружения и всё ещё может возвращать 429.",
"Too many requests": "Слишком много запросов",
"Try adjusting your search": "Попробуйте изменить условия поиска",
"WeChat Pay": "WeChat Pay",
"Your transaction history will appear here": "Ваша история транзакций появится здесь",
"Zero retention": "Без хранения данных", "Zero retention": "Без хранения данных",
"Zhipu": "Zhipu", "Zhipu": "Zhipu",
"Zhipu V4": "Zhipu V4", "Zhipu V4": "Zhipu V4",

View File

@ -4400,6 +4400,13 @@
"Your Telegram Bot Token": "Mã thông báo bot Telegram của bạn", "Your Telegram Bot Token": "Mã thông báo bot Telegram của bạn",
"Your Turnstile secret key": "Khóa bí mật Turnstile của bạn", "Your Turnstile secret key": "Khóa bí mật Turnstile của bạn",
"Your Turnstile site key": "Khóa site Turnstile của bạn", "Your Turnstile site key": "Khóa site Turnstile của bạn",
"Alipay": "Alipay",
"Please wait a moment before trying again.": "Vui lòng chờ một lát rồi thử lại.",
"This controls model request rate limiting. Web/API route throttling is configured by environment variables and may still return 429.": "Thiết lập này kiểm soát giới hạn tốc độ yêu cầu mô hình. Giới hạn tuyến Web/API được cấu hình bằng biến môi trường và vẫn có thể trả về 429.",
"Too many requests": "Quá nhiều yêu cầu",
"Try adjusting your search": "Hãy thử điều chỉnh tìm kiếm",
"WeChat Pay": "WeChat Pay",
"Your transaction history will appear here": "Lịch sử giao dịch của bạn sẽ xuất hiện ở đây",
"Zero retention": "Không lưu dữ liệu", "Zero retention": "Không lưu dữ liệu",
"Zhipu": "Zhipu", "Zhipu": "Zhipu",
"Zhipu V4": "Zhipu V4", "Zhipu V4": "Zhipu V4",

View File

@ -4400,6 +4400,13 @@
"Your Telegram Bot Token": "您的 Telegram 机器人令牌", "Your Telegram Bot Token": "您的 Telegram 机器人令牌",
"Your Turnstile secret key": "您的 Turnstile 密钥", "Your Turnstile secret key": "您的 Turnstile 密钥",
"Your Turnstile site key": "您的 Turnstile 站点密钥", "Your Turnstile site key": "您的 Turnstile 站点密钥",
"Alipay": "支付宝",
"Please wait a moment before trying again.": "请稍候再试。",
"This controls model request rate limiting. Web/API route throttling is configured by environment variables and may still return 429.": "此处仅控制模型请求速率限制。Web/API 路由限流由环境变量配置,仍可能返回 429。",
"Too many requests": "请求过于频繁",
"Try adjusting your search": "请尝试调整搜索条件",
"WeChat Pay": "微信支付",
"Your transaction history will appear here": "您的交易历史会显示在这里",
"Zero retention": "零数据保留", "Zero retention": "零数据保留",
"Zhipu": "智谱", "Zhipu": "智谱",
"Zhipu V4": "智谱 V4", "Zhipu V4": "智谱 V4",

View File

@ -19,6 +19,8 @@ import { Route as RankingsIndexRouteImport } from './routes/rankings/index'
import { Route as PricingIndexRouteImport } from './routes/pricing/index' import { Route as PricingIndexRouteImport } from './routes/pricing/index'
import { Route as AboutIndexRouteImport } from './routes/about/index' import { Route as AboutIndexRouteImport } from './routes/about/index'
import { Route as OauthProviderRouteImport } from './routes/oauth/$provider' import { Route as OauthProviderRouteImport } from './routes/oauth/$provider'
import { Route as ConsoleTopupRouteImport } from './routes/console/topup'
import { Route as ConsoleLogRouteImport } from './routes/console/log'
import { Route as AuthenticatedChat2linkRouteImport } from './routes/_authenticated/chat2link' import { Route as AuthenticatedChat2linkRouteImport } from './routes/_authenticated/chat2link'
import { Route as errors503RouteImport } from './routes/(errors)/503' import { Route as errors503RouteImport } from './routes/(errors)/503'
import { Route as errors500RouteImport } from './routes/(errors)/500' import { Route as errors500RouteImport } from './routes/(errors)/500'
@ -114,6 +116,16 @@ const OauthProviderRoute = OauthProviderRouteImport.update({
path: '/oauth/$provider', path: '/oauth/$provider',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const ConsoleTopupRoute = ConsoleTopupRouteImport.update({
id: '/console/topup',
path: '/console/topup',
getParentRoute: () => rootRouteImport,
} as any)
const ConsoleLogRoute = ConsoleLogRouteImport.update({
id: '/console/log',
path: '/console/log',
getParentRoute: () => rootRouteImport,
} as any)
const AuthenticatedChat2linkRoute = AuthenticatedChat2linkRouteImport.update({ const AuthenticatedChat2linkRoute = AuthenticatedChat2linkRouteImport.update({
id: '/chat2link', id: '/chat2link',
path: '/chat2link', path: '/chat2link',
@ -391,6 +403,8 @@ export interface FileRoutesByFullPath {
'/500': typeof errors500Route '/500': typeof errors500Route
'/503': typeof errors503Route '/503': typeof errors503Route
'/chat2link': typeof AuthenticatedChat2linkRoute '/chat2link': typeof AuthenticatedChat2linkRoute
'/console/log': typeof ConsoleLogRoute
'/console/topup': typeof ConsoleTopupRoute
'/oauth/$provider': typeof OauthProviderRoute '/oauth/$provider': typeof OauthProviderRoute
'/about/': typeof AboutIndexRoute '/about/': typeof AboutIndexRoute
'/pricing/': typeof PricingIndexRoute '/pricing/': typeof PricingIndexRoute
@ -446,6 +460,8 @@ export interface FileRoutesByTo {
'/500': typeof errors500Route '/500': typeof errors500Route
'/503': typeof errors503Route '/503': typeof errors503Route
'/chat2link': typeof AuthenticatedChat2linkRoute '/chat2link': typeof AuthenticatedChat2linkRoute
'/console/log': typeof ConsoleLogRoute
'/console/topup': typeof ConsoleTopupRoute
'/oauth/$provider': typeof OauthProviderRoute '/oauth/$provider': typeof OauthProviderRoute
'/about': typeof AboutIndexRoute '/about': typeof AboutIndexRoute
'/pricing': typeof PricingIndexRoute '/pricing': typeof PricingIndexRoute
@ -505,6 +521,8 @@ export interface FileRoutesById {
'/(errors)/500': typeof errors500Route '/(errors)/500': typeof errors500Route
'/(errors)/503': typeof errors503Route '/(errors)/503': typeof errors503Route
'/_authenticated/chat2link': typeof AuthenticatedChat2linkRoute '/_authenticated/chat2link': typeof AuthenticatedChat2linkRoute
'/console/log': typeof ConsoleLogRoute
'/console/topup': typeof ConsoleTopupRoute
'/oauth/$provider': typeof OauthProviderRoute '/oauth/$provider': typeof OauthProviderRoute
'/about/': typeof AboutIndexRoute '/about/': typeof AboutIndexRoute
'/pricing/': typeof PricingIndexRoute '/pricing/': typeof PricingIndexRoute
@ -563,6 +581,8 @@ export interface FileRouteTypes {
| '/500' | '/500'
| '/503' | '/503'
| '/chat2link' | '/chat2link'
| '/console/log'
| '/console/topup'
| '/oauth/$provider' | '/oauth/$provider'
| '/about/' | '/about/'
| '/pricing/' | '/pricing/'
@ -618,6 +638,8 @@ export interface FileRouteTypes {
| '/500' | '/500'
| '/503' | '/503'
| '/chat2link' | '/chat2link'
| '/console/log'
| '/console/topup'
| '/oauth/$provider' | '/oauth/$provider'
| '/about' | '/about'
| '/pricing' | '/pricing'
@ -676,6 +698,8 @@ export interface FileRouteTypes {
| '/(errors)/500' | '/(errors)/500'
| '/(errors)/503' | '/(errors)/503'
| '/_authenticated/chat2link' | '/_authenticated/chat2link'
| '/console/log'
| '/console/topup'
| '/oauth/$provider' | '/oauth/$provider'
| '/about/' | '/about/'
| '/pricing/' | '/pricing/'
@ -727,6 +751,8 @@ export interface RootRouteChildren {
errors404Route: typeof errors404Route errors404Route: typeof errors404Route
errors500Route: typeof errors500Route errors500Route: typeof errors500Route
errors503Route: typeof errors503Route errors503Route: typeof errors503Route
ConsoleLogRoute: typeof ConsoleLogRoute
ConsoleTopupRoute: typeof ConsoleTopupRoute
OauthProviderRoute: typeof OauthProviderRoute OauthProviderRoute: typeof OauthProviderRoute
AboutIndexRoute: typeof AboutIndexRoute AboutIndexRoute: typeof AboutIndexRoute
PricingIndexRoute: typeof PricingIndexRoute PricingIndexRoute: typeof PricingIndexRoute
@ -807,6 +833,20 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof OauthProviderRouteImport preLoaderRoute: typeof OauthProviderRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/console/topup': {
id: '/console/topup'
path: '/console/topup'
fullPath: '/console/topup'
preLoaderRoute: typeof ConsoleTopupRouteImport
parentRoute: typeof rootRouteImport
}
'/console/log': {
id: '/console/log'
path: '/console/log'
fullPath: '/console/log'
preLoaderRoute: typeof ConsoleLogRouteImport
parentRoute: typeof rootRouteImport
}
'/_authenticated/chat2link': { '/_authenticated/chat2link': {
id: '/_authenticated/chat2link' id: '/_authenticated/chat2link'
path: '/chat2link' path: '/chat2link'
@ -1271,6 +1311,8 @@ const rootRouteChildren: RootRouteChildren = {
errors404Route: errors404Route, errors404Route: errors404Route,
errors500Route: errors500Route, errors500Route: errors500Route,
errors503Route: errors503Route, errors503Route: errors503Route,
ConsoleLogRoute: ConsoleLogRoute,
ConsoleTopupRoute: ConsoleTopupRoute,
OauthProviderRoute: OauthProviderRoute, OauthProviderRoute: OauthProviderRoute,
AboutIndexRoute: AboutIndexRoute, AboutIndexRoute: AboutIndexRoute,
PricingIndexRoute: PricingIndexRoute, PricingIndexRoute: PricingIndexRoute,

View File

@ -16,6 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import { useEffect } from 'react'
import { type QueryClient } from '@tanstack/react-query' import { type QueryClient } from '@tanstack/react-query'
import { import {
createRootRouteWithContext, createRootRouteWithContext,
@ -31,11 +32,19 @@ import { NavigationProgress } from '@/components/navigation-progress'
import { GeneralError } from '@/features/errors/general-error' import { GeneralError } from '@/features/errors/general-error'
import { NotFoundError } from '@/features/errors/not-found-error' import { NotFoundError } from '@/features/errors/not-found-error'
import { getSetupStatus } from '@/features/setup/api' import { getSetupStatus } from '@/features/setup/api'
import { saveAffiliateCode } from '@/features/auth/lib/storage'
function RootComponent() { function RootComponent() {
// Load system configuration (logo, system name, etc.) from backend // Load system configuration (logo, system name, etc.) from backend
useSystemConfig({ autoLoad: true }) useSystemConfig({ autoLoad: true })
useEffect(() => {
const aff = new URLSearchParams(window.location.search).get('aff')?.trim()
if (aff) {
saveAffiliateCode(aff)
}
}, [])
return ( return (
<ThemeCustomizationProvider> <ThemeCustomizationProvider>
<NavigationProgress /> <NavigationProgress />

25
web/default/src/routes/console/log.tsx vendored Normal file
View File

@ -0,0 +1,25 @@
/*
Copyright (C) 2023-2026 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 { createFileRoute, redirect } from '@tanstack/react-router'
export const Route = createFileRoute('/console/log')({
beforeLoad: () => {
throw redirect({ to: '/usage-logs' })
},
})

View File

@ -0,0 +1,28 @@
/*
Copyright (C) 2023-2026 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 { createFileRoute, redirect } from '@tanstack/react-router'
export const Route = createFileRoute('/console/topup')({
beforeLoad: () => {
throw redirect({
to: '/wallet',
search: { show_history: true },
})
},
})