diff --git a/controller/system_info.go b/controller/system_info.go index 6126b071..acad2ccd 100644 --- a/controller/system_info.go +++ b/controller/system_info.go @@ -2,6 +2,7 @@ package controller import ( "net/http" + "strings" "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/model" @@ -28,3 +29,37 @@ func ListSystemInstances(c *gin.Context) { "data": responses, }) } + +func DeleteStaleSystemInstances(c *gin.Context) { + deletedCount, err := model.DeleteStaleSystemInstances(common.GetTimestamp()) + if err != nil { + common.ApiError(c, err) + return + } + + common.ApiSuccess(c, gin.H{ + "deleted_count": deletedCount, + }) +} + +func DeleteStaleSystemInstance(c *gin.Context) { + nodeName := c.Param("node_name") + if strings.TrimSpace(nodeName) == "" { + common.ApiErrorMsg(c, "node name is required") + return + } + + deleted, err := model.DeleteStaleSystemInstance(nodeName, common.GetTimestamp()) + if err != nil { + common.ApiError(c, err) + return + } + if !deleted { + common.ApiErrorMsg(c, "instance is not stale or no longer exists") + return + } + + common.ApiSuccess(c, gin.H{ + "deleted_count": 1, + }) +} diff --git a/model/system_instance.go b/model/system_instance.go index 93be6b31..5f343645 100644 --- a/model/system_instance.go +++ b/model/system_instance.go @@ -75,6 +75,16 @@ func ListSystemInstances() ([]*SystemInstance, error) { return instances, err } +func DeleteStaleSystemInstances(now int64) (int64, error) { + result := DB.Where("last_seen_at < ?", now-SystemInstanceStaleAfterSeconds).Delete(&SystemInstance{}) + return result.RowsAffected, result.Error +} + +func DeleteStaleSystemInstance(nodeName string, now int64) (bool, error) { + result := DB.Where("node_name = ? AND last_seen_at < ?", nodeName, now-SystemInstanceStaleAfterSeconds).Delete(&SystemInstance{}) + return result.RowsAffected > 0, result.Error +} + func (instance *SystemInstance) ToResponse(now int64) SystemInstanceResponse { status := SystemInstanceStatusOnline if now-instance.LastSeenAt > SystemInstanceStaleAfterSeconds { diff --git a/router/api-router.go b/router/api-router.go index e11b7488..83f9259b 100644 --- a/router/api-router.go +++ b/router/api-router.go @@ -288,6 +288,8 @@ func SetApiRouter(router *gin.Engine) { systemInfoRoute.Use(middleware.RootAuth()) { systemInfoRoute.GET("/instances", controller.ListSystemInstances) + systemInfoRoute.DELETE("/stale-instances", controller.DeleteStaleSystemInstances) + systemInfoRoute.DELETE("/instances/:node_name", controller.DeleteStaleSystemInstance) } dataRoute := apiRouter.Group("/data") diff --git a/web/default/src/features/system-info/api.ts b/web/default/src/features/system-info/api.ts index d571295c..6c0ae52d 100644 --- a/web/default/src/features/system-info/api.ts +++ b/web/default/src/features/system-info/api.ts @@ -18,7 +18,10 @@ For commercial licensing, please contact support@quantumnous.com */ import { api } from '@/lib/api' -import type { SystemInstanceListResponse } from './types' +import type { + SystemInstanceDeleteResponse, + SystemInstanceListResponse, +} from './types' export async function listSystemInstances() { const res = await api.get( @@ -26,3 +29,17 @@ export async function listSystemInstances() { ) return res.data } + +export async function deleteStaleSystemInstances() { + const res = await api.delete( + '/api/system-info/stale-instances' + ) + return res.data +} + +export async function deleteStaleSystemInstance(nodeName: string) { + const res = await api.delete( + `/api/system-info/instances/${encodeURIComponent(nodeName)}` + ) + return res.data +} diff --git a/web/default/src/features/system-info/components/system-instances-panel.tsx b/web/default/src/features/system-info/components/system-instances-panel.tsx index 496ed757..351f8ab1 100644 --- a/web/default/src/features/system-info/components/system-instances-panel.tsx +++ b/web/default/src/features/system-info/components/system-instances-panel.tsx @@ -16,11 +16,19 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ -import { useQuery } from '@tanstack/react-query' -import { AlertTriangle, RefreshCw, ServerCog } from 'lucide-react' -import type { ReactNode } from 'react' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { + AlertTriangle, + Loader2, + RefreshCw, + ServerCog, + Trash2, +} from 'lucide-react' +import { useState, type ReactNode } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from 'sonner' +import { ConfirmDialog } from '@/components/confirm-dialog' import { ErrorState } from '@/components/error-state' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' @@ -51,10 +59,19 @@ import { toIntlLocale } from '@/i18n/languages' import { formatTimestampRelative, formatTimestampToDate } from '@/lib/format' import { cn } from '@/lib/utils' -import { listSystemInstances } from '../api' +import { + deleteStaleSystemInstance, + deleteStaleSystemInstances, + listSystemInstances, +} from '../api' import type { SystemInstance, SystemInstanceStatus } from '../types' const INSTANCE_POLL_INTERVAL_MS = 30_000 +const INSTANCE_SKELETON_KEYS = [ + 'system-instance-skeleton-1', + 'system-instance-skeleton-2', + 'system-instance-skeleton-3', +] const STATUS_CLASS_NAME: Record = { online: @@ -210,6 +227,9 @@ function ResourceCell(props: ResourceCellProps) { type SystemInstancesTableProps = { instances: SystemInstance[] + deletingNodeName: string | null + isDeletingInstance: boolean + onDeleteStaleInstance: (instance: SystemInstance) => void } function SystemInstancesList(props: SystemInstancesTableProps) { @@ -217,7 +237,7 @@ function SystemInstancesList(props: SystemInstancesTableProps) { return (
- +
@@ -243,9 +263,12 @@ function SystemInstancesList(props: SystemInstancesTableProps) { {t('Started')} - + {t('Last Seen')} + + {t('Actions')} + @@ -254,6 +277,9 @@ function SystemInstancesList(props: SystemInstancesTableProps) { instance.info?.node?.should_configure_manually === true const resources = instance.info?.resources const storage = resources?.storage + const isDeletingThisInstance = + props.isDeletingInstance && + props.deletingNodeName === instance.node_name return ( @@ -406,7 +432,7 @@ function SystemInstancesList(props: SystemInstancesTableProps) { {formatTimestampToDate(instance.started_at)} {formatTimestampRelative( @@ -415,6 +441,45 @@ function SystemInstancesList(props: SystemInstancesTableProps) { toIntlLocale(i18n.language) )} + + {instance.status === 'stale' ? ( + + + + props.onDeleteStaleInstance(instance) + } + disabled={ + props.isDeletingInstance || + isDeletingThisInstance + } + aria-label={t('Delete stale instance')} + > + {isDeletingThisInstance ? ( + + + ) : ( + - + )} + ) })} @@ -426,6 +491,10 @@ function SystemInstancesList(props: SystemInstancesTableProps) { export function SystemInstancesPanel() { const { t } = useTranslation() + const queryClient = useQueryClient() + const [deleteTarget, setDeleteTarget] = useState(null) + const [deleteAllConfirmOpen, setDeleteAllConfirmOpen] = useState(false) + const [deletingNodeName, setDeletingNodeName] = useState(null) const instancesQuery = useQuery({ queryKey: ['system-info', 'instances'], queryFn: async () => { @@ -441,89 +510,235 @@ export function SystemInstancesPanel() { }) const instances = instancesQuery.data ?? [] + const staleInstances = instances.filter( + (instance) => instance.status === 'stale' + ) const loading = instancesQuery.isLoading const refreshing = instancesQuery.isFetching && !instancesQuery.isLoading + const invalidateInstances = async () => { + await queryClient.invalidateQueries({ + queryKey: ['system-info', 'instances'], + }) + } + + const deleteStaleInstanceMutation = useMutation({ + mutationFn: async (nodeName: string) => { + const res = await deleteStaleSystemInstance(nodeName) + if (!res.success) { + throw new Error(res.message || t('Delete failed')) + } + return res + }, + onMutate: (nodeName) => { + setDeletingNodeName(nodeName) + }, + onSuccess: async () => { + toast.success(t('Deleted stale instance')) + await invalidateInstances() + setDeleteTarget(null) + }, + onError: (error) => { + toast.error(error instanceof Error ? error.message : t('Delete failed')) + void invalidateInstances() + }, + onSettled: () => { + setDeletingNodeName(null) + }, + }) + + const deleteStaleInstancesMutation = useMutation({ + mutationFn: async () => { + const res = await deleteStaleSystemInstances() + if (!res.success) { + throw new Error(res.message || t('Delete failed')) + } + return res + }, + onSuccess: async (res) => { + toast.success( + t('Deleted {{count}} stale instances', { + count: res.data?.deleted_count ?? 0, + }) + ) + await invalidateInstances() + setDeleteAllConfirmOpen(false) + }, + onError: (error) => { + toast.error(error instanceof Error ? error.message : t('Delete failed')) + }, + }) + + const isMutatingInstance = + deletingNodeName !== null || deleteStaleInstanceMutation.isPending + + let instancesContent: ReactNode + if (loading) { + instancesContent = ( +
+ {INSTANCE_SKELETON_KEYS.map((key) => ( + + ))} +
+ ) + } else if (instancesQuery.isError) { + instancesContent = ( + { + void instancesQuery.refetch() + }} + className='min-h-[220px]' + /> + ) + } else if (instances.length === 0) { + instancesContent = ( +
+
+
+

+ {t('No instances have reported yet.')} +

+
+ ) + } else { + instancesContent = ( +
+ +
+ ) + } + return ( -
-
-
-
- - -
-

{t('Instances')}

-

- {t( - 'Nodes reporting from this deployment and their latest heartbeat.' - )} -

+ <> +
+
+
+
+ + +
+

{t('Instances')}

+

+ {t( + 'Nodes reporting from this deployment and their latest heartbeat.' + )} +

+
-
-
- - {t('Auto-refreshing every {{seconds}}s', { - seconds: INSTANCE_POLL_INTERVAL_MS / 1000, - })} - - -
-
- -
- {loading ? ( -
- {Array.from({ length: 3 }).map((_, i) => ( - - ))} -
- ) : instancesQuery.isError ? ( - { - void instancesQuery.refetch() - }} - className='min-h-[220px]' - /> - ) : instances.length === 0 ? ( -
-
- + + {t('Auto-refreshing every {{seconds}}s', { + seconds: INSTANCE_POLL_INTERVAL_MS / 1000, + })} + + +
-

- {t('No instances have reported yet.')} -

-
- ) : ( -
- + {refreshing ? t('Refreshing...') : t('Refresh')} +
+
+ +
{instancesContent}
+
+ + - + destructive + isLoading={deleteStaleInstancesMutation.isPending} + confirmText={ + deleteStaleInstancesMutation.isPending + ? t('Deleting...') + : t('Delete') + } + handleConfirm={() => deleteStaleInstancesMutation.mutate()} + /> + + { + if (!open) setDeleteTarget(null) + }} + title={t('Delete stale instance')} + desc={ + deleteTarget + ? t( + 'Delete stale instance "{{name}}"? If it has reported again, it will not be deleted.', + { name: getNodeName(deleteTarget) } + ) + : '' + } + destructive + isLoading={deleteStaleInstanceMutation.isPending} + confirmText={ + deleteStaleInstanceMutation.isPending ? t('Deleting...') : t('Delete') + } + handleConfirm={() => { + if (!deleteTarget) return + deleteStaleInstanceMutation.mutate(deleteTarget.node_name) + }} + /> + ) } diff --git a/web/default/src/features/system-info/types.ts b/web/default/src/features/system-info/types.ts index 04bfd014..611ed78e 100644 --- a/web/default/src/features/system-info/types.ts +++ b/web/default/src/features/system-info/types.ts @@ -77,3 +77,11 @@ export type SystemInstanceListResponse = { message: string data?: SystemInstance[] } + +export type SystemInstanceDeleteResponse = { + success: boolean + message: string + data?: { + deleted_count: number + } +} diff --git a/web/default/src/i18n/locales/en.json b/web/default/src/i18n/locales/en.json index e020a9df..c10cb6c8 100644 --- a/web/default/src/i18n/locales/en.json +++ b/web/default/src/i18n/locales/en.json @@ -1260,10 +1260,12 @@ "Delete": "Delete", "Delete (": "Delete (", "Delete {{count}} API key(s)?": "Delete {{count}} API key(s)?", + "Delete {{count}} stale instance records? Online instances will not be deleted.": "Delete {{count}} stale instance records? Online instances will not be deleted.", "Delete a runtime request header": "Delete a runtime request header", "Delete Account": "Delete Account", "Delete All Disabled": "Delete All Disabled", "Delete All Disabled Channels?": "Delete All Disabled Channels?", + "Delete all stale": "Delete all stale", "Delete Auto-Disabled": "Delete Auto-Disabled", "Delete Channel": "Delete Channel", "Delete Channels?": "Delete Channels?", @@ -1289,10 +1291,14 @@ "Delete selected API keys": "Delete selected API keys", "Delete selected channels": "Delete selected channels", "Delete selected models": "Delete selected models", + "Delete stale instance": "Delete stale instance", + "Delete stale instance \"{{name}}\"? If it has reported again, it will not be deleted.": "Delete stale instance \"{{name}}\"? If it has reported again, it will not be deleted.", + "Delete stale instances": "Delete stale instances", "Deleted": "Deleted", "Deleted \"{{name}}\"": "Deleted \"{{name}}\"", "Deleted ({{id}})": "Deleted ({{id}})", "Deleted {{count}} failed models": "Deleted {{count}} failed models", + "Deleted {{count}} stale instances": "Deleted {{count}} stale instances", "Deleted a custom OAuth provider": "Deleted a custom OAuth provider", "Deleted a deployment": "Deleted a deployment", "Deleted a model": "Deleted a model", @@ -1303,6 +1309,7 @@ "Deleted all disabled channels ({{count}})": "Deleted all disabled channels ({{count}})", "Deleted channel {{name}} (ID: {{id}})": "Deleted channel {{name}} (ID: {{id}})", "Deleted invalid redemption codes": "Deleted invalid redemption codes", + "Deleted stale instance": "Deleted stale instance", "Deleted successfully": "Deleted successfully", "Deleted user {{username}} (ID: {{id}})": "Deleted user {{username}} (ID: {{id}})", "Deleting will permanently remove this subscription record (including benefit details). Continue?": "Deleting will permanently remove this subscription record (including benefit details). Continue?", diff --git a/web/default/src/i18n/locales/fr.json b/web/default/src/i18n/locales/fr.json index df83593b..c8e693a3 100644 --- a/web/default/src/i18n/locales/fr.json +++ b/web/default/src/i18n/locales/fr.json @@ -1260,10 +1260,12 @@ "Delete": "Supprimer", "Delete (": "Supprimer (", "Delete {{count}} API key(s)?": "Supprimer {{count}} clé(s) API ?", + "Delete {{count}} stale instance records? Online instances will not be deleted.": "Supprimer {{count}} enregistrement(s) d'instance expirée ? Les instances en ligne ne seront pas supprimées.", "Delete a runtime request header": "Supprimer un en-tête de requête à l'exécution", "Delete Account": "Supprimer le compte", "Delete All Disabled": "Supprimer tout ce qui est désactivé", "Delete All Disabled Channels?": "Supprimer tous les canaux désactivés ?", + "Delete all stale": "Supprimer toutes les expirées", "Delete Auto-Disabled": "Supprimer les désactivés automatiquement", "Delete Channel": "Supprimer le canal", "Delete Channels?": "Supprimer les canaux ?", @@ -1289,10 +1291,14 @@ "Delete selected API keys": "Supprimer les clés API sélectionnées", "Delete selected channels": "Supprimer les canaux sélectionnés", "Delete selected models": "Supprimer les modèles sélectionnés", + "Delete stale instance": "Supprimer l'instance expirée", + "Delete stale instance \"{{name}}\"? If it has reported again, it will not be deleted.": "Supprimer l'instance expirée \"{{name}}\" ? Si elle a de nouveau signalé son état, elle ne sera pas supprimée.", + "Delete stale instances": "Supprimer les instances expirées", "Deleted": "Supprimé", "Deleted \"{{name}}\"": "\"{{name}}\" supprimé", "Deleted ({{id}})": "Supprimé ({{id}})", "Deleted {{count}} failed models": "{{count}} modèles en échec supprimés", + "Deleted {{count}} stale instances": "{{count}} instances expirées supprimées", "Deleted a custom OAuth provider": "Fournisseur OAuth personnalisé supprimé", "Deleted a deployment": "Déploiement supprimé", "Deleted a model": "Modèle supprimé", @@ -1303,6 +1309,7 @@ "Deleted all disabled channels ({{count}})": "Tous les canaux désactivés supprimés ({{count}})", "Deleted channel {{name}} (ID: {{id}})": "Canal {{name}} supprimé (ID : {{id}})", "Deleted invalid redemption codes": "Codes de réduction invalides supprimés", + "Deleted stale instance": "Instance expirée supprimée", "Deleted successfully": "Supprimé avec succès", "Deleted user {{username}} (ID: {{id}})": "Utilisateur {{username}} supprimé (ID : {{id}})", "Deleting will permanently remove this subscription record (including benefit details). Continue?": "La suppression supprimera définitivement cet enregistrement d'abonnement (y compris les détails des avantages). Continuer ?", diff --git a/web/default/src/i18n/locales/ja.json b/web/default/src/i18n/locales/ja.json index 186258f1..9c17ce9e 100644 --- a/web/default/src/i18n/locales/ja.json +++ b/web/default/src/i18n/locales/ja.json @@ -1260,10 +1260,12 @@ "Delete": "削除", "Delete (": "削除 (", "Delete {{count}} API key(s)?": "{{count}}個のAPIキーを削除しますか?", + "Delete {{count}} stale instance records? Online instances will not be deleted.": "期限切れインスタンスレコードを {{count}} 件削除しますか?オンラインのインスタンスは削除されません。", "Delete a runtime request header": "ランタイムリクエストヘッダーを削除", "Delete Account": "アカウント削除", "Delete All Disabled": "すべての無効なものを削除", "Delete All Disabled Channels?": "すべての無効なチャネルを削除しますか?", + "Delete all stale": "期限切れをすべて削除", "Delete Auto-Disabled": "自動無効化されたものを削除", "Delete Channel": "チャネルを削除", "Delete Channels?": "チャネルを削除しますか?", @@ -1289,10 +1291,14 @@ "Delete selected API keys": "選択したAPIキーを削除", "Delete selected channels": "選択したチャネルを削除", "Delete selected models": "選択したモデルを削除", + "Delete stale instance": "期限切れインスタンスを削除", + "Delete stale instance \"{{name}}\"? If it has reported again, it will not be deleted.": "期限切れインスタンス \"{{name}}\" を削除しますか?再度報告されている場合は削除されません。", + "Delete stale instances": "期限切れインスタンスを削除", "Deleted": "削除済み", "Deleted \"{{name}}\"": "\"{{name}}\" を削除しました", "Deleted ({{id}})": "削除済み ({{id}})", "Deleted {{count}} failed models": "失敗したモデルを {{count}} 個削除しました", + "Deleted {{count}} stale instances": "期限切れインスタンスを {{count}} 件削除しました", "Deleted a custom OAuth provider": "カスタム OAuth プロバイダーを削除しました", "Deleted a deployment": "デプロイメントを削除しました", "Deleted a model": "モデルを削除しました", @@ -1303,6 +1309,7 @@ "Deleted all disabled channels ({{count}})": "無効なチャネルをすべて削除しました({{count}})", "Deleted channel {{name}} (ID: {{id}})": "チャネル {{name}} を削除しました(ID: {{id}})", "Deleted invalid redemption codes": "無効な引換コードを削除しました", + "Deleted stale instance": "期限切れインスタンスを削除しました", "Deleted successfully": "削除しました", "Deleted user {{username}} (ID: {{id}})": "ユーザー {{username}} を削除しました(ID: {{id}})", "Deleting will permanently remove this subscription record (including benefit details). Continue?": "削除するとこのサブスクリプション記録(特典詳細を含む)が完全に削除されます。続行しますか?", diff --git a/web/default/src/i18n/locales/ru.json b/web/default/src/i18n/locales/ru.json index 66987733..a32e0e2e 100644 --- a/web/default/src/i18n/locales/ru.json +++ b/web/default/src/i18n/locales/ru.json @@ -1260,10 +1260,12 @@ "Delete": "Удалить", "Delete (": "Удалить (", "Delete {{count}} API key(s)?": "Удалить {{count}} API-ключ(а/ей)?", + "Delete {{count}} stale instance records? Online instances will not be deleted.": "Удалить {{count}} записей устаревших экземпляров? Онлайн-экземпляры не будут удалены.", "Delete a runtime request header": "Удалить заголовок запроса во время выполнения", "Delete Account": "Удалить аккаунт", "Delete All Disabled": "Удалить все отключенные", "Delete All Disabled Channels?": "Удалить все отключенные каналы?", + "Delete all stale": "Удалить все устаревшие", "Delete Auto-Disabled": "Удалить автоматически отключенные", "Delete Channel": "Удалить канал", "Delete Channels?": "Удалить каналы?", @@ -1289,10 +1291,14 @@ "Delete selected API keys": "Удалить выбранные ключи API", "Delete selected channels": "Удалить выбранные каналы", "Delete selected models": "Удалить выбранные модели", + "Delete stale instance": "Удалить устаревший экземпляр", + "Delete stale instance \"{{name}}\"? If it has reported again, it will not be deleted.": "Удалить устаревший экземпляр \"{{name}}\"? Если он снова отправил отчет, он не будет удален.", + "Delete stale instances": "Удалить устаревшие экземпляры", "Deleted": "Удалён", "Deleted \"{{name}}\"": "\"{{name}}\" удалено", "Deleted ({{id}})": "Удалён ({{id}})", "Deleted {{count}} failed models": "Удалено неуспешных моделей: {{count}}", + "Deleted {{count}} stale instances": "Удалено устаревших экземпляров: {{count}}", "Deleted a custom OAuth provider": "Удалён пользовательский провайдер OAuth", "Deleted a deployment": "Удалено развёртывание", "Deleted a model": "Удалена модель", @@ -1303,6 +1309,7 @@ "Deleted all disabled channels ({{count}})": "Удалены все отключённые каналы ({{count}})", "Deleted channel {{name}} (ID: {{id}})": "Удалён канал {{name}} (ID: {{id}})", "Deleted invalid redemption codes": "Недействительные коды погашения удалены", + "Deleted stale instance": "Устаревший экземпляр удален", "Deleted successfully": "Удалено успешно", "Deleted user {{username}} (ID: {{id}})": "Удалён пользователь {{username}} (ID: {{id}})", "Deleting will permanently remove this subscription record (including benefit details). Continue?": "Удаление безвозвратно удалит запись подписки (включая детали льгот). Продолжить?", diff --git a/web/default/src/i18n/locales/vi.json b/web/default/src/i18n/locales/vi.json index 6197d955..ea3dd941 100644 --- a/web/default/src/i18n/locales/vi.json +++ b/web/default/src/i18n/locales/vi.json @@ -1260,10 +1260,12 @@ "Delete": "Xóa", "Delete (": "Xóa (", "Delete {{count}} API key(s)?": "Xóa {{count}} khóa API?", + "Delete {{count}} stale instance records? Online instances will not be deleted.": "Xóa {{count}} bản ghi phiên bản mất kết nối? Các phiên bản đang trực tuyến sẽ không bị xóa.", "Delete a runtime request header": "Xóa header yêu cầu runtime", "Delete Account": "Xóa tài khoản", "Delete All Disabled": "Xóa Tất Cả Đã Tắt", "Delete All Disabled Channels?": "Xóa tất cả kênh đã vô hiệu hóa?", + "Delete all stale": "Xóa tất cả mất kết nối", "Delete Auto-Disabled": "Xóa Tự động vô hiệu hóa", "Delete Channel": "Xóa Kênh", "Delete Channels?": "Xóa các kênh?", @@ -1289,10 +1291,14 @@ "Delete selected API keys": "Xóa các khóa API đã chọn", "Delete selected channels": "Xóa các kênh đã chọn", "Delete selected models": "Xóa các mô hình đã chọn", + "Delete stale instance": "Xóa phiên bản mất kết nối", + "Delete stale instance \"{{name}}\"? If it has reported again, it will not be deleted.": "Xóa phiên bản mất kết nối \"{{name}}\"? Nếu phiên bản này đã báo cáo lại, nó sẽ không bị xóa.", + "Delete stale instances": "Xóa các phiên bản mất kết nối", "Deleted": "Đã xóa", "Deleted \"{{name}}\"": "Đã xóa \"{{name}}\"", "Deleted ({{id}})": "Đã xóa ({{id}})", "Deleted {{count}} failed models": "Đã xóa {{count}} mô hình thất bại", + "Deleted {{count}} stale instances": "Đã xóa {{count}} phiên bản mất kết nối", "Deleted a custom OAuth provider": "Đã xóa nhà cung cấp OAuth tùy chỉnh", "Deleted a deployment": "Đã xóa một triển khai", "Deleted a model": "Đã xóa một mô hình", @@ -1303,6 +1309,7 @@ "Deleted all disabled channels ({{count}})": "Đã xóa tất cả kênh bị vô hiệu hóa ({{count}})", "Deleted channel {{name}} (ID: {{id}})": "Đã xóa kênh {{name}} (ID: {{id}})", "Deleted invalid redemption codes": "Đã xóa các mã đổi thưởng không hợp lệ", + "Deleted stale instance": "Đã xóa phiên bản mất kết nối", "Deleted successfully": "Xóa thành công", "Deleted user {{username}} (ID: {{id}})": "Đã xóa người dùng {{username}} (ID: {{id}})", "Deleting will permanently remove this subscription record (including benefit details). Continue?": "Xóa sẽ xóa vĩnh viễn bản ghi đăng ký này (bao gồm chi tiết quyền lợi). Tiếp tục?", diff --git a/web/default/src/i18n/locales/zh.json b/web/default/src/i18n/locales/zh.json index 7315160d..d70316e3 100644 --- a/web/default/src/i18n/locales/zh.json +++ b/web/default/src/i18n/locales/zh.json @@ -1260,10 +1260,12 @@ "Delete": "删除", "Delete (": "删除 (", "Delete {{count}} API key(s)?": "删除 {{count}} 个 API 密钥?", + "Delete {{count}} stale instance records? Online instances will not be deleted.": "删除 {{count}} 条失联实例记录?在线实例不会被删除。", "Delete a runtime request header": "删除运行期请求头", "Delete Account": "删除账户", "Delete All Disabled": "删除所有已禁用", "Delete All Disabled Channels?": "删除所有已禁用的渠道?", + "Delete all stale": "删除所有失联", "Delete Auto-Disabled": "删除自动禁用", "Delete Channel": "删除渠道", "Delete Channels?": "删除渠道?", @@ -1289,10 +1291,14 @@ "Delete selected API keys": "删除选定的 API 密钥", "Delete selected channels": "删除所选渠道", "Delete selected models": "删除选定的模型", + "Delete stale instance": "删除失联实例", + "Delete stale instance \"{{name}}\"? If it has reported again, it will not be deleted.": "删除失联实例 \"{{name}}\"?如果它已经重新上报,将不会被删除。", + "Delete stale instances": "删除失联实例", "Deleted": "已注销", "Deleted \"{{name}}\"": "已删除 \"{{name}}\"", "Deleted ({{id}})": "已删除({{id}})", "Deleted {{count}} failed models": "已删除 {{count}} 个失败模型", + "Deleted {{count}} stale instances": "已删除 {{count}} 个失联实例", "Deleted a custom OAuth provider": "删除了一个自定义 OAuth 提供方", "Deleted a deployment": "删除了一个部署", "Deleted a model": "删除了一个模型", @@ -1303,6 +1309,7 @@ "Deleted all disabled channels ({{count}})": "删除全部禁用渠道({{count}})", "Deleted channel {{name}} (ID: {{id}})": "删除渠道 {{name}}(ID: {{id}})", "Deleted invalid redemption codes": "删除无效兑换码", + "Deleted stale instance": "已删除失联实例", "Deleted successfully": "删除成功", "Deleted user {{username}} (ID: {{id}})": "删除用户 {{username}}(ID: {{id}})", "Deleting will permanently remove this subscription record (including benefit details). Continue?": "删除会彻底移除该订阅记录(含权益明细)。是否继续?",