mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-09-06 17:46:23 +00:00
feat: add persistent system task log cleanup progress
This commit is contained in:
+30
@@ -21,7 +21,9 @@ import type {
|
||||
ConfirmPaymentComplianceResponse,
|
||||
DeleteLogsResponse,
|
||||
FetchUpstreamRatiosRequest,
|
||||
LogCleanupTask,
|
||||
SystemOptionsResponse,
|
||||
SystemTaskResponse,
|
||||
UpdateOptionRequest,
|
||||
UpdateOptionResponse,
|
||||
UpstreamChannelsResponse,
|
||||
@@ -53,6 +55,34 @@ export async function deleteLogsBefore(targetTimestamp: number) {
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function startLogCleanupTask(targetTimestamp: number) {
|
||||
const res = await api.post<SystemTaskResponse<LogCleanupTask>>(
|
||||
'/api/system-task/log-cleanup',
|
||||
null,
|
||||
{
|
||||
params: { target_timestamp: targetTimestamp },
|
||||
}
|
||||
)
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function getCurrentLogCleanupTask() {
|
||||
const res = await api.get<SystemTaskResponse<LogCleanupTask | null>>(
|
||||
'/api/system-task/current',
|
||||
{
|
||||
params: { type: 'log_cleanup' },
|
||||
}
|
||||
)
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function getSystemTask(taskId: string) {
|
||||
const res = await api.get<SystemTaskResponse<LogCleanupTask>>(
|
||||
`/api/system-task/${taskId}`
|
||||
)
|
||||
return res.data
|
||||
}
|
||||
|
||||
export async function resetModelRatios() {
|
||||
const res = await api.post<UpdateOptionResponse>(
|
||||
'/api/option/rest_model_ratio'
|
||||
|
||||
+122
-16
@@ -48,6 +48,7 @@ import {
|
||||
} from '@/components/ui/form'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Progress } from '@/components/ui/progress'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -59,7 +60,11 @@ import {
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { DateTimePicker } from '@/components/datetime-picker'
|
||||
import { deleteLogsBefore } from '../api'
|
||||
import {
|
||||
getCurrentLogCleanupTask,
|
||||
getSystemTask,
|
||||
startLogCleanupTask,
|
||||
} from '../api'
|
||||
import {
|
||||
SettingsControlGroup,
|
||||
SettingsForm,
|
||||
@@ -69,6 +74,7 @@ import {
|
||||
import { SettingsPageFormActions } from '../components/settings-page-context'
|
||||
import { SettingsSection } from '../components/settings-section'
|
||||
import { useUpdateOption } from '../hooks/use-update-option'
|
||||
import type { LogCleanupTask } from '../types'
|
||||
|
||||
const logSettingsSchema = z.object({
|
||||
LogConsumeEnabled: z.boolean(),
|
||||
@@ -127,6 +133,10 @@ const quickSelectOptions = [
|
||||
},
|
||||
]
|
||||
|
||||
function isActiveLogCleanupTask(task: LogCleanupTask | null) {
|
||||
return task?.status === 'pending' || task?.status === 'running'
|
||||
}
|
||||
|
||||
export function LogSettingsSection({
|
||||
defaultEnabled,
|
||||
}: LogSettingsSectionProps) {
|
||||
@@ -142,7 +152,10 @@ export function LogSettingsSection({
|
||||
const [purgeDate, setPurgeDate] = useState<Date | undefined>(() =>
|
||||
getDateDaysAgo(30)
|
||||
)
|
||||
const [isCleaning, setIsCleaning] = useState(false)
|
||||
const [isStartingLogCleanup, setIsStartingLogCleanup] = useState(false)
|
||||
const [logCleanupTask, setLogCleanupTask] = useState<LogCleanupTask | null>(
|
||||
null
|
||||
)
|
||||
const [showConfirmDialog, setShowConfirmDialog] = useState(false)
|
||||
const [serverLogInfo, setServerLogInfo] = useState<ServerLogInfo | null>(
|
||||
null
|
||||
@@ -168,6 +181,27 @@ export function LogSettingsSection({
|
||||
fetchServerLogInfo()
|
||||
}, [fetchServerLogInfo])
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
|
||||
async function fetchCurrentLogCleanupTask() {
|
||||
try {
|
||||
const res = await getCurrentLogCleanupTask()
|
||||
if (!cancelled && res.success && res.data) {
|
||||
setLogCleanupTask(res.data)
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
fetchCurrentLogCleanupTask()
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [])
|
||||
|
||||
const purgeTimestamp = useMemo(() => {
|
||||
if (!purgeDate) return null
|
||||
return Math.floor(purgeDate.getTime() / 1000)
|
||||
@@ -178,6 +212,49 @@ export function LogSettingsSection({
|
||||
return formatTimestampToDate(purgeDate.getTime(), 'milliseconds')
|
||||
}, [purgeDate])
|
||||
|
||||
const logCleanupActive = isActiveLogCleanupTask(logCleanupTask)
|
||||
const logCleanupState = logCleanupTask?.state
|
||||
const logCleanupProgress = Math.min(
|
||||
100,
|
||||
Math.max(0, logCleanupState?.progress ?? 0)
|
||||
)
|
||||
const logCleanupProcessed = logCleanupState?.processed ?? 0
|
||||
const logCleanupTotal = logCleanupState?.total ?? 0
|
||||
|
||||
useEffect(() => {
|
||||
if (!logCleanupTask || !isActiveLogCleanupTask(logCleanupTask)) return
|
||||
|
||||
let cancelled = false
|
||||
const interval = window.setInterval(async () => {
|
||||
try {
|
||||
const res = await getSystemTask(logCleanupTask.task_id)
|
||||
if (cancelled || !res.success || !res.data) return
|
||||
|
||||
setLogCleanupTask(res.data)
|
||||
if (!isActiveLogCleanupTask(res.data)) {
|
||||
if (res.data.status === 'succeeded') {
|
||||
const count =
|
||||
res.data.result?.deleted_count ?? res.data.state?.processed ?? 0
|
||||
toast.success(
|
||||
count > 0
|
||||
? t('{{count}} log entries removed.', { count })
|
||||
: t('No log entries matched the selected time.')
|
||||
)
|
||||
} else if (res.data.status === 'failed') {
|
||||
toast.error(res.data.error || t('Failed to clean logs'))
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* keep polling */
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
window.clearInterval(interval)
|
||||
}
|
||||
}, [logCleanupTask?.task_id, logCleanupTask?.status, t])
|
||||
|
||||
const onSubmit = async (values: LogSettingsFormValues) => {
|
||||
if (values.LogConsumeEnabled === defaultEnabled) return
|
||||
await updateOption.mutateAsync({
|
||||
@@ -201,24 +278,24 @@ export function LogSettingsSection({
|
||||
return
|
||||
}
|
||||
|
||||
setIsCleaning(true)
|
||||
setIsStartingLogCleanup(true)
|
||||
try {
|
||||
const res = await deleteLogsBefore(purgeTimestamp)
|
||||
const res = await startLogCleanupTask(purgeTimestamp)
|
||||
if (!res.success) {
|
||||
throw new Error(res.message || t('Failed to clean logs'))
|
||||
}
|
||||
const count = res.data ?? 0
|
||||
toast.success(
|
||||
count > 0
|
||||
? t('{{count}} log entries removed.', { count })
|
||||
: t('No log entries matched the selected time.')
|
||||
)
|
||||
if (!res.data) {
|
||||
throw new Error(t('Failed to clean logs'))
|
||||
}
|
||||
setLogCleanupTask(res.data)
|
||||
setShowConfirmDialog(false)
|
||||
toast.success(t('Log cleanup task started.'))
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : t('Failed to clean logs')
|
||||
toast.error(message)
|
||||
} finally {
|
||||
setIsCleaning(false)
|
||||
setIsStartingLogCleanup(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -314,11 +391,37 @@ export function LogSettingsSection({
|
||||
type='button'
|
||||
variant='destructive'
|
||||
onClick={handleRequestCleanLogs}
|
||||
disabled={isCleaning}
|
||||
disabled={isStartingLogCleanup || logCleanupActive}
|
||||
>
|
||||
{isCleaning ? t('Cleaning...') : t('Clean logs')}
|
||||
{isStartingLogCleanup || logCleanupActive
|
||||
? t('Cleaning...')
|
||||
: t('Clean logs')}
|
||||
</Button>
|
||||
</div>
|
||||
{logCleanupTask && (
|
||||
<div className='rounded-md border p-3'>
|
||||
<div className='mb-2 flex items-center justify-between gap-3 text-sm'>
|
||||
<span className='font-medium'>
|
||||
{t('Log cleanup progress')}
|
||||
</span>
|
||||
<span className='text-muted-foreground tabular-nums'>
|
||||
{logCleanupProgress}%
|
||||
</span>
|
||||
</div>
|
||||
<Progress value={logCleanupProgress} />
|
||||
<div className='text-muted-foreground mt-2 text-xs'>
|
||||
{t('{{processed}} of {{total}} log entries processed.', {
|
||||
processed: logCleanupProcessed,
|
||||
total: logCleanupTotal,
|
||||
})}
|
||||
</div>
|
||||
{logCleanupTask.status === 'failed' && logCleanupTask.error && (
|
||||
<div className='text-destructive mt-2 text-xs'>
|
||||
{logCleanupTask.error}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</SettingsControlGroup>
|
||||
</SettingsForm>
|
||||
</Form>
|
||||
@@ -491,11 +594,14 @@ export function LogSettingsSection({
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isCleaning}>
|
||||
<AlertDialogCancel disabled={isStartingLogCleanup}>
|
||||
{t('Cancel')}
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleCleanLogs} disabled={isCleaning}>
|
||||
{isCleaning ? t('Cleaning...') : t('Delete logs')}
|
||||
<AlertDialogAction
|
||||
onClick={handleCleanLogs}
|
||||
disabled={isStartingLogCleanup}
|
||||
>
|
||||
{isStartingLogCleanup ? t('Cleaning...') : t('Delete logs')}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
|
||||
@@ -56,6 +56,56 @@ export type DeleteLogsResponse = {
|
||||
data?: number
|
||||
}
|
||||
|
||||
export type SystemTaskStatus = 'pending' | 'running' | 'succeeded' | 'failed'
|
||||
|
||||
export type SystemTask<
|
||||
TPayload = Record<string, unknown>,
|
||||
TState = Record<string, unknown>,
|
||||
TResult = Record<string, unknown>,
|
||||
> = {
|
||||
id: number
|
||||
task_id: string
|
||||
type: string
|
||||
status: SystemTaskStatus
|
||||
active_key?: string
|
||||
payload?: TPayload
|
||||
state?: TState
|
||||
result?: TResult
|
||||
error?: string
|
||||
locked_by?: string
|
||||
locked_until?: number
|
||||
created_at: number
|
||||
updated_at: number
|
||||
}
|
||||
|
||||
export type LogCleanupTaskPayload = {
|
||||
target_timestamp: number
|
||||
batch_size: number
|
||||
}
|
||||
|
||||
export type LogCleanupTaskState = {
|
||||
total: number
|
||||
processed: number
|
||||
progress: number
|
||||
remaining: number
|
||||
}
|
||||
|
||||
export type LogCleanupTaskResult = {
|
||||
deleted_count: number
|
||||
}
|
||||
|
||||
export type LogCleanupTask = SystemTask<
|
||||
LogCleanupTaskPayload,
|
||||
LogCleanupTaskState,
|
||||
LogCleanupTaskResult
|
||||
>
|
||||
|
||||
export type SystemTaskResponse<TTask = SystemTask | null> = {
|
||||
success: boolean
|
||||
message: string
|
||||
data?: TTask
|
||||
}
|
||||
|
||||
export type SiteSettings = {
|
||||
'theme.frontend': string
|
||||
Notice: string
|
||||
|
||||
Vendored
+3
@@ -52,6 +52,7 @@
|
||||
"{{modality}} not supported": "{{modality}} not supported",
|
||||
"{{modality}} supported": "{{modality}} supported",
|
||||
"{{n}} model(s) selected": "{{n}} model(s) selected",
|
||||
"{{processed}} of {{total}} log entries processed.": "{{processed}} of {{total}} log entries processed.",
|
||||
"{{success}} succeeded, {{failed}} failed": "{{success}} succeeded, {{failed}} failed",
|
||||
"{{target}} test failed": "{{target}} test failed",
|
||||
"{{target}} test succeeded": "{{target}} test succeeded",
|
||||
@@ -2352,6 +2353,8 @@
|
||||
"Locations": "Locations",
|
||||
"Locked": "Locked",
|
||||
"log": "log",
|
||||
"Log cleanup progress": "Log cleanup progress",
|
||||
"Log cleanup task started.": "Log cleanup task started.",
|
||||
"Log Details": "Log Details",
|
||||
"Log Directory": "Log Directory",
|
||||
"Log File Count": "Log File Count",
|
||||
|
||||
Vendored
+3
@@ -52,6 +52,7 @@
|
||||
"{{modality}} not supported": "{{modality}} non pris en charge",
|
||||
"{{modality}} supported": "{{modality}} pris en charge",
|
||||
"{{n}} model(s) selected": "{{n}} modèle(s) sélectionné(s)",
|
||||
"{{processed}} of {{total}} log entries processed.": "{{processed}} sur {{total}} entrées de journal traitées.",
|
||||
"{{success}} succeeded, {{failed}} failed": "{{success}} réussi(s), {{failed}} échoué(s)",
|
||||
"{{target}} test failed": "Échec du test de {{target}}",
|
||||
"{{target}} test succeeded": "Test de {{target}} réussi",
|
||||
@@ -2352,6 +2353,8 @@
|
||||
"Locations": "Emplacements",
|
||||
"Locked": "Verrouillé",
|
||||
"log": "journal",
|
||||
"Log cleanup progress": "Progression du nettoyage des journaux",
|
||||
"Log cleanup task started.": "La tâche de nettoyage des journaux a démarré.",
|
||||
"Log Details": "Détails du journal",
|
||||
"Log Directory": "Répertoire des journaux",
|
||||
"Log File Count": "Nombre de fichiers journaux",
|
||||
|
||||
Vendored
+3
@@ -52,6 +52,7 @@
|
||||
"{{modality}} not supported": "{{modality}} はサポートされていません",
|
||||
"{{modality}} supported": "{{modality}} をサポート",
|
||||
"{{n}} model(s) selected": "{{n}} 件のモデルを選択済み",
|
||||
"{{processed}} of {{total}} log entries processed.": "{{total}} 件中 {{processed}} 件のログを処理しました。",
|
||||
"{{success}} succeeded, {{failed}} failed": "{{success}} 件成功、{{failed}} 件失敗",
|
||||
"{{target}} test failed": "{{target}} のテストに失敗しました",
|
||||
"{{target}} test succeeded": "{{target}} のテストに成功しました",
|
||||
@@ -2352,6 +2353,8 @@
|
||||
"Locations": "場所",
|
||||
"Locked": "ロック済み",
|
||||
"log": "ログ",
|
||||
"Log cleanup progress": "ログクリーンアップの進行状況",
|
||||
"Log cleanup task started.": "ログクリーンアップタスクを開始しました。",
|
||||
"Log Details": "ログの詳細",
|
||||
"Log Directory": "ログディレクトリ",
|
||||
"Log File Count": "ログファイル数",
|
||||
|
||||
Vendored
+3
@@ -52,6 +52,7 @@
|
||||
"{{modality}} not supported": "{{modality}} не поддерживается",
|
||||
"{{modality}} supported": "{{modality}} поддерживается",
|
||||
"{{n}} model(s) selected": "Выбрано моделей: {{n}}",
|
||||
"{{processed}} of {{total}} log entries processed.": "Обработано {{processed}} из {{total}} записей журнала.",
|
||||
"{{success}} succeeded, {{failed}} failed": "{{success}} успешно, {{failed}} с ошибкой",
|
||||
"{{target}} test failed": "Тест {{target}} не выполнен",
|
||||
"{{target}} test succeeded": "Тест {{target}} успешно выполнен",
|
||||
@@ -2352,6 +2353,8 @@
|
||||
"Locations": "Местоположения",
|
||||
"Locked": "Заблокировано",
|
||||
"log": "записи",
|
||||
"Log cleanup progress": "Ход очистки журнала",
|
||||
"Log cleanup task started.": "Задача очистки журнала запущена.",
|
||||
"Log Details": "Детали журнала",
|
||||
"Log Directory": "Каталог журналов",
|
||||
"Log File Count": "Кол-во файлов журналов",
|
||||
|
||||
Vendored
+3
@@ -52,6 +52,7 @@
|
||||
"{{modality}} not supported": "Không hỗ trợ {{modality}}",
|
||||
"{{modality}} supported": "Hỗ trợ {{modality}}",
|
||||
"{{n}} model(s) selected": "Đã chọn {{n}} model",
|
||||
"{{processed}} of {{total}} log entries processed.": "Đã xử lý {{processed}}/{{total}} mục nhật ký.",
|
||||
"{{success}} succeeded, {{failed}} failed": "{{success}} thành công, {{failed}} thất bại",
|
||||
"{{target}} test failed": "Kiểm tra {{target}} thất bại",
|
||||
"{{target}} test succeeded": "Kiểm tra {{target}} thành công",
|
||||
@@ -2352,6 +2353,8 @@
|
||||
"Locations": "Vị trí",
|
||||
"Locked": "Đã khóa",
|
||||
"log": "nhật ký",
|
||||
"Log cleanup progress": "Tiến trình dọn dẹp nhật ký",
|
||||
"Log cleanup task started.": "Đã bắt đầu tác vụ dọn dẹp nhật ký.",
|
||||
"Log Details": "Chi tiết Nhật ký",
|
||||
"Log Directory": "Thư mục nhật ký",
|
||||
"Log File Count": "Số tệp nhật ký",
|
||||
|
||||
Vendored
+3
@@ -52,6 +52,7 @@
|
||||
"{{modality}} not supported": "不支持 {{modality}}",
|
||||
"{{modality}} supported": "支持 {{modality}}",
|
||||
"{{n}} model(s) selected": "已选 {{n}} 个模型",
|
||||
"{{processed}} of {{total}} log entries processed.": "已处理 {{processed}} / {{total}} 条日志。",
|
||||
"{{success}} succeeded, {{failed}} failed": "{{success}} 个成功,{{failed}} 个失败",
|
||||
"{{target}} test failed": "{{target}} 测试失败",
|
||||
"{{target}} test succeeded": "{{target}} 测试成功",
|
||||
@@ -2352,6 +2353,8 @@
|
||||
"Locations": "位置",
|
||||
"Locked": "锁定",
|
||||
"log": "日志的完整详情",
|
||||
"Log cleanup progress": "日志清理进度",
|
||||
"Log cleanup task started.": "日志清理任务已启动。",
|
||||
"Log Details": "日志详情",
|
||||
"Log Directory": "日志目录",
|
||||
"Log File Count": "日志文件数",
|
||||
|
||||
Reference in New Issue
Block a user