Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 36 additions & 2 deletions makefile
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
FRONTEND_DIR = ./web/default
FRONTEND_CLASSIC_DIR = ./web/classic
BACKEND_DIR = .
DEV_COMPOSE_FILE = docker-compose.dev.yml
DEV_POSTGRES_SERVICE = postgres
DEV_BACKEND_SERVICE = new-api
DEV_POSTGRES_DB = new-api
DEV_POSTGRES_USER = root
DEV_SQLITE_PATH ?= one-api.db

.PHONY: all build-frontend build-frontend-classic build-all-frontends start-backend dev dev-api dev-web dev-web-classic
.PHONY: all build-frontend build-frontend-classic build-all-frontends start-backend dev dev-api dev-api-rebuild dev-web dev-web-classic reset-setup

all: build-all-frontends start-backend

Expand All @@ -22,7 +28,11 @@ start-backend:

dev-api:
@echo "Starting backend services (docker)..."
@docker compose -f docker-compose.dev.yml up -d
@docker compose -f $(DEV_COMPOSE_FILE) up -d

dev-api-rebuild:
@echo "Rebuilding and starting backend service (docker)..."
@docker compose -f $(DEV_COMPOSE_FILE) up -d --build $(DEV_BACKEND_SERVICE)

dev-web:
@echo "Starting frontend dev server..."
Expand All @@ -33,3 +43,27 @@ dev-web-classic:
@cd $(FRONTEND_CLASSIC_DIR) && bun install && bun run dev

dev: dev-api dev-web

reset-setup:
@echo "Resetting local setup wizard state..."
@if docker compose -f $(DEV_COMPOSE_FILE) ps --services --status running | grep -qx "$(DEV_POSTGRES_SERVICE)"; then \
echo "Detected running docker dev PostgreSQL. Removing setup record and root users..."; \
docker compose -f $(DEV_COMPOSE_FILE) exec -T $(DEV_POSTGRES_SERVICE) \
psql -U $(DEV_POSTGRES_USER) -d $(DEV_POSTGRES_DB) \
-c 'DELETE FROM setups;' \
-c 'DELETE FROM users WHERE role = 100;' \
-c "DELETE FROM options WHERE key IN ('SelfUseModeEnabled', 'DemoSiteEnabled');"; \
echo "Restarting docker dev backend so setup status is recalculated..."; \
docker compose -f $(DEV_COMPOSE_FILE) restart $(DEV_BACKEND_SERVICE); \
elif db_path="$${SQLITE_PATH:-$(DEV_SQLITE_PATH)}"; db_path="$${db_path%%\?*}"; [ -f "$$db_path" ]; then \
db_path="$${SQLITE_PATH:-$(DEV_SQLITE_PATH)}"; \
db_path="$${db_path%%\?*}"; \
echo "Detected local SQLite database: $$db_path"; \
sqlite3 "$$db_path" \
"DELETE FROM setups; DELETE FROM users WHERE role = 100; DELETE FROM options WHERE key IN ('SelfUseModeEnabled', 'DemoSiteEnabled');"; \
echo "SQLite setup state reset. Restart the local backend process before testing the setup wizard."; \
Comment on lines +49 to +64
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

python - <<'PY'
from pathlib import Path

lines = Path("makefile").read_text().splitlines()
start = next(i for i, line in enumerate(lines, 1) if line.startswith("reset-setup:"))
end = len(lines) + 1
for i in range(start + 1, len(lines) + 1):
    line = lines[i - 1]
    if line and not line.startswith("\t"):
        end = i
        break

block = lines[start - 1:end - 1]
for i, line in enumerate(block, start):
    print(f"{i}:{line}")

print("\nHas fail-fast guard:", any("set -e" in line for line in block))
print("Has Postgres transaction:", any("BEGIN;" in line and "psql" not in line for line in block))
print("Has SQLite transaction:", any('sqlite3 "$$db_path"' in line for line in block) and any("BEGIN;" in line and "sqlite3" not in line for line in block))
PY

Repository: QuantumNous/new-api

Length of output: 1671


Add fail-fast and transaction wrappers to prevent silent failures.

The reset-setup target lacks a fail-fast guard (set -e) and transactional wrappers around database operations. If psql or sqlite3 fails, the subsequent echo and restart commands still execute, causing the target to exit 0 even though the setup state was not cleared. Wrap both database branches with transaction commands and add a fail-fast guard at the start.

Suggested fix
 reset-setup:
 	`@echo` "Resetting local setup wizard state..."
-	`@if` docker compose -f $(DEV_COMPOSE_FILE) ps --services --status running | grep -qx "$(DEV_POSTGRES_SERVICE)"; then \
+	`@set` -e; \
+	if docker compose -f $(DEV_COMPOSE_FILE) ps --services --status running | grep -qx "$(DEV_POSTGRES_SERVICE)"; then \
 		echo "Detected running docker dev PostgreSQL. Removing setup record and root users..."; \
 		docker compose -f $(DEV_COMPOSE_FILE) exec -T $(DEV_POSTGRES_SERVICE) \
-			psql -U $(DEV_POSTGRES_USER) -d $(DEV_POSTGRES_DB) \
-			-c 'DELETE FROM setups;' \
-			-c 'DELETE FROM users WHERE role = 100;' \
-			-c "DELETE FROM options WHERE key IN ('SelfUseModeEnabled', 'DemoSiteEnabled');"; \
+			psql -v ON_ERROR_STOP=1 -U $(DEV_POSTGRES_USER) -d $(DEV_POSTGRES_DB) \
+			-c 'BEGIN;' \
+			-c 'DELETE FROM setups;' \
+			-c 'DELETE FROM users WHERE role = 100;' \
+			-c "DELETE FROM options WHERE key IN ('SelfUseModeEnabled', 'DemoSiteEnabled');" \
+			-c 'COMMIT;'; \
 		echo "Restarting docker dev backend so setup status is recalculated..."; \
 		docker compose -f $(DEV_COMPOSE_FILE) restart $(DEV_BACKEND_SERVICE); \
 	elif db_path="$${SQLITE_PATH:-$(DEV_SQLITE_PATH)}"; db_path="$${db_path%%\?*}"; [ -f "$$db_path" ]; then \
 		db_path="$${SQLITE_PATH:-$(DEV_SQLITE_PATH)}"; \
 		db_path="$${db_path%%\?*}"; \
 		echo "Detected local SQLite database: $$db_path"; \
 		sqlite3 "$$db_path" \
-			"DELETE FROM setups; DELETE FROM users WHERE role = 100; DELETE FROM options WHERE key IN ('SelfUseModeEnabled', 'DemoSiteEnabled');"; \
+			"BEGIN; DELETE FROM setups; DELETE FROM users WHERE role = 100; DELETE FROM options WHERE key IN ('SelfUseModeEnabled', 'DemoSiteEnabled'); COMMIT;"; \
 		echo "SQLite setup state reset. Restart the local backend process before testing the setup wizard."; \
 	else \
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@makefile` around lines 49 - 64, Add fail-fast and transactional guards to the
reset-setup target: enable shell fail-fast by prefixing the multi-line recipe
branches with "set -e;" and for the Postgres branch call psql with ON_ERROR_STOP
(psql -v ON_ERROR_STOP=1) and wrap the SQL in a transaction (BEGIN; ...;
COMMIT;), and for the SQLite branch wrap the statements in a transaction (BEGIN
TRANSACTION; ...; COMMIT;) so any SQL error aborts and prevents the subsequent
echo/restart steps; locate and modify the Docker/Postgres exec line that invokes
psql and the sqlite3 invocation to include these changes.

else \
echo "No running docker dev PostgreSQL or local SQLite database found."; \
echo "Start the dev stack with 'make dev-api', or set SQLITE_PATH/DEV_SQLITE_PATH to your local SQLite database."; \
exit 1; \
fi
21 changes: 10 additions & 11 deletions web/default/src/components/language-switcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { useCallback } from 'react'
import {
INTERFACE_LANGUAGE_OPTIONS,
normalizeInterfaceLanguage,
} from '@/i18n/languages'
import { Languages, Check } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { useAuthStore } from '@/stores/auth-store'
Expand All @@ -30,18 +34,10 @@ import {
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'

const languages = [
{ code: 'en', label: 'English' },
{ code: 'zh', label: '中文' },
{ code: 'fr', label: 'Français' },
{ code: 'ru', label: 'Русский' },
{ code: 'ja', label: '日本語' },
{ code: 'vi', label: 'Tiếng Việt' },
]

export function LanguageSwitcher() {
const { i18n, t } = useTranslation()
const user = useAuthStore((s) => s.auth.user)
const currentLanguage = normalizeInterfaceLanguage(i18n.language)

const handleChangeLanguage = useCallback(
async (code: string) => {
Expand All @@ -66,15 +62,18 @@ export function LanguageSwitcher() {
<span className='sr-only'>{t('Change language')}</span>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
{languages.map((lang) => (
{INTERFACE_LANGUAGE_OPTIONS.map((lang) => (
<DropdownMenuItem
key={lang.code}
onClick={() => handleChangeLanguage(lang.code)}
>
{lang.label}
<Check
size={14}
className={cn('ms-auto', i18n.language !== lang.code && 'hidden')}
className={cn(
'ms-auto',
currentLanguage !== lang.code && 'hidden'
)}
/>
</DropdownMenuItem>
))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@ function ChatMenuItem({
/>
}
>
<span>{preset.name}</span>
<span className='min-w-0 flex-1 truncate whitespace-nowrap'>
{preset.name}
</span>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
)
Expand All @@ -95,11 +97,13 @@ function ChatMenuItem({
isActive={false}
className='justify-between'
>
<span>{preset.name}</span>
<span className='min-w-0 flex-1 truncate whitespace-nowrap'>
{preset.name}
</span>
{loading ? (
<Loader2 className='h-4 w-4 animate-spin' />
<Loader2 className='h-4 w-4 shrink-0 animate-spin' />
) : (
<ExternalLink className='h-4 w-4' />
<ExternalLink className='h-4 w-4 shrink-0' />
)}
</SidebarMenuSubButton>
</SidebarMenuSubItem>
Expand Down
6 changes: 5 additions & 1 deletion web/default/src/components/ui/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
type FieldValues,
} from 'react-hook-form'
import { useRender } from '@base-ui/react/use-render'
import { useTranslation } from 'react-i18next'
import { cn } from '@/lib/utils'
import { Label } from '@/components/ui/label'

Expand Down Expand Up @@ -153,20 +154,23 @@ function FormDescription({ className, ...props }: React.ComponentProps<'p'>) {

function FormMessage({ className, ...props }: React.ComponentProps<'p'>) {
const { error, formMessageId } = useFormField()
const { t } = useTranslation()
const body = error ? String(error?.message ?? '') : props.children

if (!body) {
return null
}

const translatedBody = typeof body === 'string' ? t(body) : body

return (
<p
data-slot='form-message'
id={formMessageId}
className={cn('text-destructive text-sm', className)}
{...props}
>
{body}
{translatedBody}
</p>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,13 +232,20 @@ export function ChannelTestDialog({
} catch (error: unknown) {
updateTestResult(model, {
status: 'error',
error: error instanceof Error ? error.message : 'Test failed',
error: error instanceof Error ? error.message : t('Test failed'),
})
} finally {
markModelTesting(model, false)
}
},
[currentRow, endpointType, isStreamTest, markModelTesting, updateTestResult]
[
currentRow,
endpointType,
isStreamTest,
markModelTesting,
t,
updateTestResult,
]
)

const handleBatchTest = useCallback(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ export function MultiKeyManageDialog({
}
} catch (error: unknown) {
toast.error(
error instanceof Error ? error.message : 'Failed to load key status'
error instanceof Error ? error.message : t('Failed to load key status')
)
} finally {
setIsLoading(false)
Expand Down Expand Up @@ -181,7 +181,7 @@ export function MultiKeyManageDialog({
}

if (response?.success) {
toast.success(response.message || 'Operation successful')
toast.success(response.message || t('Operation successful'))
queryClient.invalidateQueries({ queryKey: channelsQueryKeys.lists() })

// Reload data - reset to page 1 for bulk actions
Expand All @@ -193,10 +193,12 @@ export function MultiKeyManageDialog({
loadKeyStatus(currentPage, pageSize)
}
} else {
toast.error(response?.message || 'Operation failed')
toast.error(response?.message || t('Operation failed'))
}
} catch (error: unknown) {
toast.error(error instanceof Error ? error.message : 'Operation failed')
toast.error(
error instanceof Error ? error.message : t('Operation failed')
)
} finally {
setIsPerformingAction(false)
setConfirmAction(null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -698,7 +698,7 @@ export function ChannelMutateDrawer({
try {
const res = await getChannelKey(channelId)
if (!res.success) {
throw new Error(res.message || 'Failed to fetch channel key')
throw new Error(res.message || t('Failed to fetch channel key'))
}

const keyValue = res.data?.key ?? ''
Expand Down Expand Up @@ -733,7 +733,7 @@ export function ChannelMutateDrawer({
try {
const res = await refreshCodexCredential(channelId)
if (!res.success) {
throw new Error(res.message || 'Failed to refresh credential')
throw new Error(res.message || t('Failed to refresh credential'))
}
toast.success(t('Credential refreshed'))
queryClient.invalidateQueries({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { useEffect, useMemo, useState } from 'react'
import {
INTERFACE_LANGUAGE_OPTIONS,
normalizeInterfaceLanguage,
} from '@/i18n/languages'
import { Languages, Loader2 } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
Expand All @@ -34,24 +38,6 @@ import { updateUserLanguage } from '../api'
import { parseUserSettings } from '../lib'
import type { UserProfile } from '../types'

const LANGUAGE_OPTIONS = [
{ value: 'zh', label: '简体中文' },
{ value: 'en', label: 'English' },
{ value: 'fr', label: 'Français' },
{ value: 'ru', label: 'Русский' },
{ value: 'ja', label: '日本語' },
{ value: 'vi', label: 'Tiếng Việt' },
] as const

function normalizeLanguage(value?: string | null): string {
if (!value) return 'en'
const normalized = value.trim().replace(/_/g, '-').toLowerCase()
if (normalized.startsWith('zh')) return 'zh'
return LANGUAGE_OPTIONS.some((lang) => lang.value === normalized)
? normalized
: 'en'
}

type LanguagePreferencesCardProps = {
profile: UserProfile | null
onProfileUpdate: () => void
Expand All @@ -64,7 +50,7 @@ export function LanguagePreferencesCard(props: LanguagePreferencesCardProps) {

const savedLanguage = useMemo(() => {
const settings = parseUserSettings(props.profile?.setting)
return normalizeLanguage(settings.language || i18n.language)
return normalizeInterfaceLanguage(settings.language || i18n.language)
}, [props.profile?.setting, i18n.language])

const [currentLanguage, setCurrentLanguage] = useState(savedLanguage)
Expand All @@ -75,7 +61,7 @@ export function LanguagePreferencesCard(props: LanguagePreferencesCardProps) {

const handleLanguageChange = async (language: string | null) => {
if (!language) return
const nextLanguage = normalizeLanguage(language)
const nextLanguage = normalizeInterfaceLanguage(language)
if (nextLanguage === currentLanguage) return

const previousLanguage = currentLanguage
Expand Down Expand Up @@ -132,8 +118,8 @@ export function LanguagePreferencesCard(props: LanguagePreferencesCardProps) {
<div className='flex items-center gap-2 sm:min-w-48'>
<Select
items={[
...LANGUAGE_OPTIONS.map((language) => ({
value: language.value,
...INTERFACE_LANGUAGE_OPTIONS.map((language) => ({
value: language.code,
label: language.label,
})),
]}
Expand All @@ -146,8 +132,8 @@ export function LanguagePreferencesCard(props: LanguagePreferencesCardProps) {
</SelectTrigger>
<SelectContent alignItemWithTrigger={false}>
<SelectGroup>
{LANGUAGE_OPTIONS.map((language) => (
<SelectItem key={language.value} value={language.value}>
{INTERFACE_LANGUAGE_OPTIONS.map((language) => (
<SelectItem key={language.code} value={language.code}>
{language.label}
</SelectItem>
))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ export function DashboardSection({ defaultValues }: DashboardSectionProps) {
items={[
...granularityOptions.map((option) => ({
value: option.value,
label: option.label,
label: t(option.label),
})),
]}
onValueChange={field.onChange}
Expand All @@ -167,7 +167,7 @@ export function DashboardSection({ defaultValues }: DashboardSectionProps) {
<SelectGroup>
{granularityOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
{t(option.label)}
</SelectItem>
))}
</SelectGroup>
Expand Down
41 changes: 41 additions & 0 deletions web/default/src/i18n/languages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
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
*/

export const INTERFACE_LANGUAGE_OPTIONS = [
{ code: 'zh', label: '简体中文' },
{ code: 'en', label: 'English' },
{ code: 'fr', label: 'Français' },
{ code: 'ru', label: 'Русский' },
{ code: 'ja', label: '日本語' },
{ code: 'vi', label: 'Tiếng Việt' },
] as const

export type InterfaceLanguageCode =
(typeof INTERFACE_LANGUAGE_OPTIONS)[number]['code']

export function normalizeInterfaceLanguage(value?: string | null): string {
if (!value) return 'en'

const normalized = value.trim().replace(/_/g, '-').toLowerCase()
if (normalized.startsWith('zh')) return 'zh'

return INTERFACE_LANGUAGE_OPTIONS.some((lang) => lang.code === normalized)
? normalized
: 'en'
}
Loading