Commit 2cbdfa03 by Calcium-Ion Committed by GitHub

feat: add system instance info panel (#5716)

* feat: add system instance reporting

* feat: show system instance resources

* fix: update translations for heartbeat messages in Russian and Vietnamese
parent 53771922
package controller
import (
"net/http"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/model"
"github.com/gin-gonic/gin"
)
func ListSystemInstances(c *gin.Context) {
instances, err := model.ListSystemInstances()
if err != nil {
common.ApiError(c, err)
return
}
now := common.GetTimestamp()
responses := make([]model.SystemInstanceResponse, 0, len(instances))
for _, instance := range instances {
responses = append(responses, instance.ToResponse(now))
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
"data": responses,
})
}
......@@ -117,6 +117,10 @@ func main() {
// Subscription quota reset task (daily/weekly/monthly/custom)
service.StartSubscriptionQuotaResetTask()
// Report this process as a system instance so the System Info page can show
// all currently alive nodes in multi-instance deployments.
service.StartSystemInstanceReporter()
// Wire task polling adaptor factory (breaks service -> relay import cycle).
// Must run before the system task runner starts: the async_task_poll handler
// calls service.RunTaskPollingOnce, which needs this factory set.
......
......@@ -294,6 +294,7 @@ func migrateDB() error {
&CustomOAuthProvider{},
&UserOAuthBinding{},
&PerfMetric{},
&SystemInstance{},
&SystemTask{},
&SystemTaskLock{},
)
......@@ -345,6 +346,7 @@ func migrateDBFast() error {
{&CustomOAuthProvider{}, "CustomOAuthProvider"},
{&UserOAuthBinding{}, "UserOAuthBinding"},
{&PerfMetric{}, "PerfMetric"},
{&SystemInstance{}, "SystemInstance"},
{&SystemTask{}, "SystemTask"},
{&SystemTaskLock{}, "SystemTaskLock"},
}
......
package model
import (
"github.com/QuantumNous/new-api/common"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
const (
SystemInstanceStatusOnline = "online"
SystemInstanceStatusStale = "stale"
SystemInstanceStaleAfterSeconds int64 = 90
)
type SystemInstance struct {
NodeName string `json:"node_name" gorm:"type:varchar(128);primaryKey"`
Info string `json:"info" gorm:"type:text"`
StartedAt int64 `json:"started_at" gorm:"bigint;index"`
LastSeenAt int64 `json:"last_seen_at" gorm:"bigint;index"`
CreatedAt int64 `json:"created_at" gorm:"bigint;index"`
UpdatedAt int64 `json:"updated_at" gorm:"bigint;index"`
}
type SystemInstanceResponse struct {
NodeName string `json:"node_name"`
Status string `json:"status"`
StaleAfterSeconds int64 `json:"stale_after_seconds"`
StartedAt int64 `json:"started_at"`
LastSeenAt int64 `json:"last_seen_at"`
Info any `json:"info"`
}
func (instance *SystemInstance) BeforeCreate(_ *gorm.DB) error {
now := common.GetTimestamp()
if instance.CreatedAt == 0 {
instance.CreatedAt = now
}
if instance.UpdatedAt == 0 {
instance.UpdatedAt = now
}
return nil
}
func UpsertSystemInstance(nodeName string, info any, startedAt int64, lastSeenAt int64) error {
infoText, err := marshalSystemInstanceInfo(info)
if err != nil {
return err
}
if lastSeenAt == 0 {
lastSeenAt = common.GetTimestamp()
}
instance := &SystemInstance{
NodeName: nodeName,
Info: infoText,
StartedAt: startedAt,
LastSeenAt: lastSeenAt,
UpdatedAt: lastSeenAt,
}
return DB.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "node_name"}},
DoUpdates: clause.AssignmentColumns([]string{
"info",
"started_at",
"last_seen_at",
"updated_at",
}),
}).Create(instance).Error
}
func ListSystemInstances() ([]*SystemInstance, error) {
var instances []*SystemInstance
err := DB.Order("last_seen_at desc").Find(&instances).Error
return instances, err
}
func (instance *SystemInstance) ToResponse(now int64) SystemInstanceResponse {
status := SystemInstanceStatusOnline
if now-instance.LastSeenAt > SystemInstanceStaleAfterSeconds {
status = SystemInstanceStatusStale
}
return SystemInstanceResponse{
NodeName: instance.NodeName,
Status: status,
StaleAfterSeconds: SystemInstanceStaleAfterSeconds,
StartedAt: instance.StartedAt,
LastSeenAt: instance.LastSeenAt,
Info: decodeSystemInstanceInfo(instance.Info),
}
}
func marshalSystemInstanceInfo(v any) (string, error) {
if v == nil {
return "", nil
}
data, err := common.Marshal(v)
if err != nil {
return "", err
}
return string(data), nil
}
func decodeSystemInstanceInfo(data string) any {
if data == "" {
return nil
}
var value any
if err := common.UnmarshalJsonStr(data, &value); err != nil {
return data
}
return value
}
......@@ -48,6 +48,7 @@ func TestMain(m *testing.M) {
&UserSubscription{},
&UserOAuthBinding{},
&PerfMetric{},
&SystemInstance{},
&SystemTask{},
&SystemTaskLock{},
); err != nil {
......@@ -73,6 +74,7 @@ func truncateTables(t *testing.T) {
DB.Exec("DELETE FROM user_subscriptions")
DB.Exec("DELETE FROM user_oauth_bindings")
DB.Exec("DELETE FROM perf_metrics")
DB.Exec("DELETE FROM system_instances")
DB.Exec("DELETE FROM system_task_locks")
DB.Exec("DELETE FROM system_tasks")
})
......
......@@ -322,6 +322,11 @@ func SetApiRouter(router *gin.Engine) {
systemTaskRoute.GET("/current", controller.GetCurrentSystemTask)
systemTaskRoute.GET("/:task_id", controller.GetSystemTask)
}
systemInfoRoute := apiRouter.Group("/system-info")
systemInfoRoute.Use(middleware.RootAuth())
{
systemInfoRoute.GET("/instances", controller.ListSystemInstances)
}
dataRoute := apiRouter.Group("/data")
dataRoute.GET("/", middleware.AdminAuth(), controller.GetAllQuotaDates)
......
package service
import (
"context"
"fmt"
"os"
"runtime"
"strings"
"sync"
"time"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/logger"
"github.com/QuantumNous/new-api/model"
"github.com/bytedance/gopkg/util/gopool"
)
const systemInstanceReportInterval = 30 * time.Second
var systemInstanceReporterOnce sync.Once
type SystemInstanceInfo struct {
SchemaVersion int `json:"schema_version"`
Node common.NodeIdentity `json:"node"`
Role SystemInstanceRoleInfo `json:"role"`
Runtime SystemInstanceRuntimeInfo `json:"runtime"`
Host SystemInstanceHostInfo `json:"host"`
Resources SystemInstanceResources `json:"resources,omitempty"`
Extra map[string]any `json:"extra,omitempty"`
}
type SystemInstanceRoleInfo struct {
IsMaster bool `json:"is_master"`
}
type SystemInstanceRuntimeInfo struct {
Version string `json:"version"`
GOOS string `json:"goos"`
GOARCH string `json:"goarch"`
StartedAt int64 `json:"started_at"`
}
type SystemInstanceHostInfo struct {
Hostname string `json:"hostname"`
}
type SystemInstanceResources struct {
CPU SystemInstanceResourceUsage `json:"cpu"`
Memory SystemInstanceResourceUsage `json:"memory"`
Storage SystemInstanceStorageMetrics `json:"storage"`
}
type SystemInstanceResourceUsage struct {
UsagePercent float64 `json:"usage_percent"`
}
type SystemInstanceStorageMetrics struct {
TotalBytes uint64 `json:"total_bytes"`
UsedBytes uint64 `json:"used_bytes"`
FreeBytes uint64 `json:"free_bytes"`
UsedPercent float64 `json:"used_percent"`
}
func StartSystemInstanceReporter() {
systemInstanceReporterOnce.Do(func() {
gopool.Go(func() {
reportSystemInstanceWithLog()
ticker := time.NewTicker(systemInstanceReportInterval)
defer ticker.Stop()
for range ticker.C {
reportSystemInstanceWithLog()
}
})
})
}
func ReportCurrentSystemInstance() error {
identity := common.GetNodeIdentity()
hostname, hostnameErr := os.Hostname()
if strings.TrimSpace(identity.Name) == "" {
if hostnameErr != nil || strings.TrimSpace(hostname) == "" {
return fmt.Errorf("system instance node name is empty")
}
identity.Name = hostname
identity.Source = common.NodeNameSourceHostname
identity.ManuallyConfigured = false
identity.ShouldConfigureManually = true
}
systemStatus := common.GetSystemStatus()
diskInfo := common.GetDiskSpaceInfo()
info := SystemInstanceInfo{
SchemaVersion: 1,
Node: identity,
Role: SystemInstanceRoleInfo{
IsMaster: common.IsMasterNode,
},
Runtime: SystemInstanceRuntimeInfo{
Version: common.Version,
GOOS: runtime.GOOS,
GOARCH: runtime.GOARCH,
StartedAt: common.StartTime,
},
Host: SystemInstanceHostInfo{
Hostname: hostname,
},
Resources: SystemInstanceResources{
CPU: SystemInstanceResourceUsage{
UsagePercent: systemStatus.CPUUsage,
},
Memory: SystemInstanceResourceUsage{
UsagePercent: systemStatus.MemoryUsage,
},
Storage: SystemInstanceStorageMetrics{
TotalBytes: diskInfo.Total,
UsedBytes: diskInfo.Used,
FreeBytes: diskInfo.Free,
UsedPercent: diskInfo.UsedPercent,
},
},
}
return model.UpsertSystemInstance(identity.Name, info, common.StartTime, common.GetTimestamp())
}
func reportSystemInstanceWithLog() {
if err := ReportCurrentSystemInstance(); err != nil {
logger.LogWarn(context.Background(), fmt.Sprintf("system instance report failed: %v", err))
}
}
/*
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 { api } from '@/lib/api'
import type { SystemInstanceListResponse } from './types'
export async function listSystemInstances() {
const res = await api.get<SystemInstanceListResponse>(
'/api/system-info/instances'
)
return res.data
}
/*
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 { useQuery } from '@tanstack/react-query'
import { AlertTriangle, RefreshCw, ServerCog } from 'lucide-react'
import type { ReactNode } from 'react'
import { useTranslation } from 'react-i18next'
import { formatTimestampRelative, formatTimestampToDate } from '@/lib/format'
import { cn } from '@/lib/utils'
import { ErrorState } from '@/components/error-state'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import {
Popover,
PopoverContent,
PopoverDescription,
PopoverHeader,
PopoverTitle,
PopoverTrigger,
} from '@/components/ui/popover'
import { Skeleton } from '@/components/ui/skeleton'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip'
import { listSystemInstances } from '../api'
import type { SystemInstance, SystemInstanceStatus } from '../types'
const INSTANCE_POLL_INTERVAL_MS = 30_000
const STATUS_CLASS_NAME: Record<SystemInstanceStatus, string> = {
online:
'bg-emerald-50 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-300',
stale: 'bg-amber-50 text-amber-700 dark:bg-amber-500/15 dark:text-amber-300',
}
const STATUS_DOT_CLASS_NAME: Record<SystemInstanceStatus, string> = {
online: 'bg-emerald-500',
stale: 'bg-amber-500',
}
function roleLabel(instance: SystemInstance) {
if (instance.info?.role?.is_master) return 'master'
return 'worker'
}
function roleDescriptionKey(instance: SystemInstance) {
if (instance.info?.role?.is_master) {
return 'Master instances run scheduled background tasks.'
}
return 'Worker instances do not run master-only background tasks.'
}
function runtimeLabel(instance: SystemInstance) {
const runtime = instance.info?.runtime
if (!runtime?.goos && !runtime?.goarch) return '-'
const parts: string[] = []
if (runtime.goos || runtime.goarch) {
parts.push([runtime.goos, runtime.goarch].filter(Boolean).join('/'))
}
return parts.join(' · ')
}
function getNodeName(instance: SystemInstance) {
return instance.info?.node?.name || instance.node_name
}
function formatPercent(value?: number) {
if (typeof value !== 'number' || Number.isNaN(value)) return '-'
return `${new Intl.NumberFormat(undefined, {
maximumFractionDigits: 1,
}).format(value)}%`
}
function formatBytes(bytes?: number): string {
if (typeof bytes !== 'number' || Number.isNaN(bytes)) return '-'
if (bytes === 0) return '0 B'
if (bytes < 0) return `-${formatBytes(-bytes)}`
const units = ['B', 'KB', 'MB', 'GB', 'TB']
const index = Math.min(
Math.floor(Math.log(bytes) / Math.log(1024)),
units.length - 1
)
const value = bytes / 1024 ** index
return `${new Intl.NumberFormat(undefined, {
maximumFractionDigits: index === 0 ? 0 : 1,
}).format(value)} ${units[index]}`
}
function ringColorClass(percent: number | null) {
if (percent === null) return 'text-muted-foreground/40'
if (percent >= 90) return 'text-red-500'
if (percent >= 70) return 'text-amber-500'
return 'text-emerald-500'
}
type RingProgressProps = {
percent: number | null
size?: number
}
function RingProgress(props: RingProgressProps) {
const size = props.size ?? 22
const stroke = 2.5
const radius = (size - stroke) / 2
const circumference = 2 * Math.PI * radius
const offset =
props.percent === null
? circumference
: circumference - (props.percent / 100) * circumference
return (
<svg
width={size}
height={size}
viewBox={`0 0 ${size} ${size}`}
className='shrink-0 -rotate-90'
aria-hidden='true'
>
<circle
cx={size / 2}
cy={size / 2}
r={radius}
fill='none'
strokeWidth={stroke}
stroke='currentColor'
className='text-muted'
/>
<circle
cx={size / 2}
cy={size / 2}
r={radius}
fill='none'
strokeWidth={stroke}
strokeLinecap='round'
stroke='currentColor'
strokeDasharray={circumference}
strokeDashoffset={offset}
className={cn(
'transition-[stroke-dashoffset] duration-500',
ringColorClass(props.percent)
)}
/>
</svg>
)
}
type ResourceCellProps = {
value?: number
tooltip?: ReactNode
}
function ResourceCell(props: ResourceCellProps) {
const percent =
typeof props.value === 'number' && !Number.isNaN(props.value)
? Math.max(0, Math.min(100, props.value))
: null
const content = (
<div className='flex items-center gap-2'>
<RingProgress percent={percent} />
<span className='font-mono text-[11px] tabular-nums'>
{formatPercent(props.value)}
</span>
</div>
)
if (!props.tooltip) return content
return (
<TooltipProvider delay={100}>
<Tooltip>
<TooltipTrigger className='block w-full rounded-sm text-left focus-visible:ring-2 focus-visible:outline-none'>
{content}
</TooltipTrigger>
<TooltipContent className='max-w-80'>{props.tooltip}</TooltipContent>
</Tooltip>
</TooltipProvider>
)
}
type SystemInstancesTableProps = {
instances: SystemInstance[]
}
function SystemInstancesList(props: SystemInstancesTableProps) {
const { t, i18n } = useTranslation()
return (
<div className='overflow-x-auto rounded-md border'>
<Table className='min-w-[1140px]'>
<TableHeader>
<TableRow className='bg-muted/40 hover:bg-muted/40'>
<TableHead className='h-9 min-w-[240px] px-4 text-xs'>
{t('Instances')}
</TableHead>
<TableHead className='h-9 w-[110px] text-xs'>
{t('Status')}
</TableHead>
<TableHead className='h-9 w-[100px] text-xs'>{t('Role')}</TableHead>
<TableHead className='h-9 w-[96px] text-xs'>{t('CPU')}</TableHead>
<TableHead className='h-9 w-[96px] text-xs'>{t('Memory')}</TableHead>
<TableHead className='h-9 w-[96px] text-xs'>
{t('Storage')}
</TableHead>
<TableHead className='h-9 w-[100px] text-xs'>
{t('Version')}
</TableHead>
<TableHead className='h-9 w-[140px] text-xs'>
{t('Runtime')}
</TableHead>
<TableHead className='h-9 w-[170px] text-xs'>
{t('Started')}
</TableHead>
<TableHead className='h-9 w-[170px] pr-4 text-xs'>
{t('Last Seen')}
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{props.instances.map((instance) => {
const shouldConfigure =
instance.info?.node?.should_configure_manually === true
const resources = instance.info?.resources
const storage = resources?.storage
return (
<TableRow key={instance.node_name} className='hover:bg-muted/30'>
<TableCell className='px-4 py-2.5 align-middle'>
<div className='flex min-w-0 items-center gap-2'>
<span
className={cn(
'size-2 shrink-0 rounded-full',
STATUS_DOT_CLASS_NAME[instance.status]
)}
aria-hidden='true'
/>
<div className='min-w-0'>
<div className='flex min-w-0 items-center gap-1.5'>
<span className='truncate text-sm font-medium'>
{getNodeName(instance)}
</span>
{shouldConfigure && (
<Popover>
<PopoverTrigger
className='inline-flex shrink-0 rounded-full focus-visible:ring-2 focus-visible:outline-none'
aria-label={t('Configure NODE_NAME')}
>
<Badge
variant='outline'
className='border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-500/30 dark:bg-amber-500/15 dark:text-amber-300'
>
<AlertTriangle
className='size-3'
aria-hidden='true'
/>
</Badge>
</PopoverTrigger>
<PopoverContent align='start' className='w-80'>
<PopoverHeader>
<PopoverTitle>
{t('Configure NODE_NAME')}
</PopoverTitle>
<PopoverDescription>
{t(
'This instance is using an automatic hostname. Set NODE_NAME to a stable unique value for multi-instance management.'
)}
</PopoverDescription>
</PopoverHeader>
<div className='space-y-2 text-xs'>
<div>
<div className='mb-1 font-medium'>
{t('Example')}
</div>
<code className='bg-muted block rounded-md px-2 py-1.5 font-mono text-[11px] break-all'>
NODE_NAME=new-api-master-1
</code>
</div>
<p className='text-muted-foreground'>
{t(
'Use a different stable value for each instance, then restart the service.'
)}
</p>
</div>
</PopoverContent>
</Popover>
)}
</div>
<div className='text-muted-foreground truncate font-mono text-[11px]'>
{instance.info?.host?.hostname || '-'}
</div>
</div>
</div>
</TableCell>
<TableCell className='py-2.5 align-middle'>
<Badge
variant='secondary'
className={cn('gap-1.5', STATUS_CLASS_NAME[instance.status])}
>
<span
className={cn(
'size-1.5 rounded-full',
STATUS_DOT_CLASS_NAME[instance.status]
)}
aria-hidden='true'
/>
{t(instance.status)}
</Badge>
</TableCell>
<TableCell className='py-2.5 align-middle'>
<TooltipProvider delay={100}>
<Tooltip>
<TooltipTrigger
className='inline-flex shrink-0 rounded-full focus-visible:ring-2 focus-visible:outline-none'
aria-label={t('Node role')}
>
<Badge variant='outline'>{roleLabel(instance)}</Badge>
</TooltipTrigger>
<TooltipContent>
{t(roleDescriptionKey(instance))}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</TableCell>
<TableCell className='py-2.5 align-middle'>
<ResourceCell value={resources?.cpu?.usage_percent} />
</TableCell>
<TableCell className='py-2.5 align-middle'>
<ResourceCell value={resources?.memory?.usage_percent} />
</TableCell>
<TableCell className='py-2.5 align-middle'>
<ResourceCell
value={storage?.used_percent}
tooltip={
storage ? (
<div className='space-y-1 text-xs'>
<div className='grid grid-cols-[auto_1fr] gap-x-3 gap-y-1'>
<span className='text-muted-foreground'>
{t('Used')}
</span>
<span className='font-mono'>
{formatBytes(storage.used_bytes)}
</span>
<span className='text-muted-foreground'>
{t('Free')}
</span>
<span className='font-mono'>
{formatBytes(storage.free_bytes)}
</span>
<span className='text-muted-foreground'>
{t('Total')}
</span>
<span className='font-mono'>
{formatBytes(storage.total_bytes)}
</span>
</div>
</div>
) : undefined
}
/>
</TableCell>
<TableCell className='py-2.5 align-middle'>
<div className='truncate font-mono text-xs'>
{instance.info?.runtime?.version || '-'}
</div>
</TableCell>
<TableCell className='py-2.5 align-middle'>
<div className='truncate font-mono text-xs'>
{runtimeLabel(instance)}
</div>
</TableCell>
<TableCell className='text-muted-foreground py-2.5 text-xs whitespace-nowrap align-middle'>
{formatTimestampToDate(instance.started_at)}
</TableCell>
<TableCell
className='text-muted-foreground py-2.5 pr-4 text-xs whitespace-nowrap align-middle'
title={formatTimestampToDate(instance.last_seen_at)}
>
{formatTimestampRelative(
instance.last_seen_at,
'seconds',
i18n.language
)}
</TableCell>
</TableRow>
)
})}
</TableBody>
</Table>
</div>
)
}
export function SystemInstancesPanel() {
const { t } = useTranslation()
const instancesQuery = useQuery({
queryKey: ['system-info', 'instances'],
queryFn: async () => {
const res = await listSystemInstances()
if (!res.success || !Array.isArray(res.data)) {
throw new Error(res.message || t('We could not load instances.'))
}
return res.data
},
staleTime: 30 * 1000,
retry: false,
refetchInterval: INSTANCE_POLL_INTERVAL_MS,
})
const instances = instancesQuery.data ?? []
const loading = instancesQuery.isLoading
const refreshing = instancesQuery.isFetching && !instancesQuery.isLoading
return (
<section className='bg-card overflow-hidden rounded-lg border shadow-xs'>
<div className='flex flex-col gap-3 border-b px-4 py-3 sm:flex-row sm:items-center sm:justify-between sm:px-5'>
<div className='min-w-0'>
<div className='flex items-center gap-2'>
<span className='bg-muted text-muted-foreground inline-flex size-7 items-center justify-center rounded-md'>
<ServerCog className='size-4' aria-hidden='true' />
</span>
<div className='min-w-0'>
<h3 className='text-sm font-semibold'>{t('Instances')}</h3>
<p className='text-muted-foreground mt-0.5 text-xs'>
{t(
'Nodes reporting from this deployment and their latest heartbeat.'
)}
</p>
</div>
</div>
</div>
<div className='flex shrink-0 items-center gap-3'>
<span className='text-muted-foreground text-xs' aria-live='polite'>
{t('Auto-refreshing every {{seconds}}s', {
seconds: INSTANCE_POLL_INTERVAL_MS / 1000,
})}
</span>
<Button
type='button'
variant='outline'
size='sm'
onClick={() => void instancesQuery.refetch()}
disabled={instancesQuery.isFetching}
aria-label={t('Refresh')}
>
<RefreshCw
data-icon='inline-start'
className={cn('size-3.5', refreshing && 'animate-spin')}
aria-hidden='true'
/>
{refreshing ? t('Refreshing...') : t('Refresh')}
</Button>
</div>
</div>
<div aria-busy={instancesQuery.isFetching}>
{loading ? (
<div className='space-y-2 p-4 sm:p-5'>
{Array.from({ length: 3 }).map((_, i) => (
<Skeleton key={i} className='h-9 w-full rounded-md' />
))}
</div>
) : instancesQuery.isError ? (
<ErrorState
title={t('We could not load instances.')}
description={
instancesQuery.error instanceof Error
? instancesQuery.error.message
: undefined
}
onRetry={() => {
void instancesQuery.refetch()
}}
className='min-h-[220px]'
/>
) : instances.length === 0 ? (
<div className='px-4 py-10 text-center sm:px-5'>
<div className='bg-muted mx-auto mb-3 flex size-10 items-center justify-center rounded-lg'>
<ServerCog
className='text-muted-foreground size-5'
aria-hidden='true'
/>
</div>
<p className='text-muted-foreground text-sm'>
{t('No instances have reported yet.')}
</p>
</div>
) : (
<div className='p-4 sm:p-5'>
<SystemInstancesList instances={instances} />
</div>
)}
</div>
</section>
)
}
......@@ -19,7 +19,7 @@ For commercial licensing, please contact support@quantumnous.com
import { useQuery } from '@tanstack/react-query'
import { ListChecks, RefreshCw } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { formatTimestampToDate } from '@/lib/format'
import { formatTimestampRelative, formatTimestampToDate } from '@/lib/format'
import { cn } from '@/lib/utils'
import { ErrorState } from '@/components/error-state'
import { Badge } from '@/components/ui/badge'
......@@ -98,7 +98,7 @@ type SystemTasksTableProps = {
}
function SystemTasksTable(props: SystemTasksTableProps) {
const { t } = useTranslation()
const { t, i18n } = useTranslation()
return (
<div className='overflow-x-auto rounded-md border'>
......@@ -172,8 +172,15 @@ function SystemTasksTable(props: SystemTasksTableProps) {
<TableCell className='text-muted-foreground max-w-[280px] truncate py-3 font-mono text-xs align-middle'>
{task.locked_by || '-'}
</TableCell>
<TableCell className='text-muted-foreground py-3 text-xs whitespace-nowrap align-middle'>
{formatTimestampToDate(task.updated_at)}
<TableCell
className='text-muted-foreground py-3 text-xs whitespace-nowrap align-middle'
title={formatTimestampToDate(task.updated_at)}
>
{formatTimestampRelative(
task.updated_at,
'seconds',
i18n.language
)}
</TableCell>
<TableCell
className='text-destructive max-w-[220px] truncate py-3 pr-4 text-xs align-middle'
......
......@@ -18,6 +18,8 @@ For commercial licensing, please contact support@quantumnous.com
*/
import { useTranslation } from 'react-i18next'
import { SectionPageLayout } from '@/components/layout'
import { Badge } from '@/components/ui/badge'
import { SystemInstancesPanel } from './components/system-instances-panel'
import { SystemTasksPanel } from './components/system-tasks-panel'
export function SystemInfo() {
......@@ -25,9 +27,19 @@ export function SystemInfo() {
return (
<SectionPageLayout>
<SectionPageLayout.Title>{t('System Info')}</SectionPageLayout.Title>
<SectionPageLayout.Title>
<span className='inline-flex min-w-0 items-center gap-2'>
<span className='truncate'>{t('System Info')}</span>
<Badge variant='outline' className='shrink-0'>
Root
</Badge>
</span>
</SectionPageLayout.Title>
<SectionPageLayout.Content>
<SystemTasksPanel />
<div className='space-y-4'>
<SystemInstancesPanel />
<SystemTasksPanel />
</div>
</SectionPageLayout.Content>
</SectionPageLayout>
)
......
/*
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 type SystemInstanceStatus = 'online' | 'stale'
export type SystemInstanceInfo = {
schema_version?: number
node?: {
name?: string
source?: string
manually_configured?: boolean
should_configure_manually?: boolean
[key: string]: unknown
}
role?: {
is_master?: boolean
[key: string]: unknown
}
runtime?: {
version?: string
goos?: string
goarch?: string
started_at?: number
[key: string]: unknown
}
host?: {
hostname?: string
[key: string]: unknown
}
resources?: {
cpu?: {
usage_percent?: number
[key: string]: unknown
}
memory?: {
usage_percent?: number
[key: string]: unknown
}
storage?: {
total_bytes?: number
used_bytes?: number
free_bytes?: number
used_percent?: number
[key: string]: unknown
}
[key: string]: unknown
}
[key: string]: unknown
}
export type SystemInstance = {
node_name: string
status: SystemInstanceStatus
stale_after_seconds: number
started_at: number
last_seen_at: number
info?: SystemInstanceInfo
}
export type SystemInstanceListResponse = {
success: boolean
message: string
data?: SystemInstance[]
}
......@@ -915,6 +915,7 @@
"Configure keyword filtering for prompts and responses.": "Configure keyword filtering for prompts and responses.",
"Configure model, caching, and group ratios used for billing": "Configure model, caching, and group ratios used for billing",
"Configure monitoring status page groups for the dashboard": "Configure monitoring status page groups for the dashboard",
"Configure NODE_NAME": "Configure NODE_NAME",
"Configure per-model ratio for image inputs or outputs.": "Configure per-model ratio for image inputs or outputs.",
"Configure per-tool unit prices ($/1K calls). Per-request models do not incur additional tool fees.": "Configure per-tool unit prices ($/1K calls). Per-request models do not incur additional tool fees.",
"Configure pricing ratios for a specific model.": "Configure pricing ratios for a specific model.",
......@@ -1052,6 +1053,7 @@
"Cost Tracking": "Cost Tracking",
"Count must be between {{min}} and {{max}}": "Count must be between {{min}} and {{max}}",
"Coze": "Coze",
"CPU": "CPU",
"CPU Threshold (%)": "CPU Threshold (%)",
"Create": "Create",
"Create a copy of:": "Create a copy of:",
......@@ -1935,6 +1937,7 @@
"Format: AppId|SecretId|SecretKey": "Format: AppId|SecretId|SecretKey",
"Forward requests directly to upstream providers without any post-processing.": "Forward requests directly to upstream providers without any post-processing.",
"Frames per second": "Frames per second",
"Free": "Free",
"Free: {{free}} / Total: {{total}}": "Free: {{free}} / Total: {{total}}",
"Friendly name to identify this channel": "Friendly name to identify this channel",
"From Address": "From Address",
......@@ -2186,6 +2189,7 @@
"Inspect requests, errors, and billing details": "Inspect requests, errors, and billing details",
"Inspect user prompts": "Inspect user prompts",
"Instance": "Instance",
"Instances": "Instances",
"Insufficient balance": "Insufficient balance",
"Integrations": "Integrations",
"Inter-group overrides": "Inter-group overrides",
......@@ -2411,6 +2415,7 @@
"Map upstream status codes to different codes": "Map upstream status codes to different codes",
"Market Share": "Market Share",
"Marketing": "Marketing",
"Master instances run scheduled background tasks.": "Master instances run scheduled background tasks.",
"Match All (AND)": "Match All (AND)",
"Match Any (OR)": "Match Any (OR)",
"Match Mode": "Match Mode",
......@@ -2449,6 +2454,7 @@
"Media pricing": "Media pricing",
"Median time-to-first-token (TTFT) sampled hourly per group": "Median time-to-first-token (TTFT) sampled hourly per group",
"Medical Q&A, mental health support": "Medical Q&A, mental health support",
"Memory": "Memory",
"Memory Hits": "Memory Hits",
"Memory Threshold (%)": "Memory Threshold (%)",
"Merchant ID": "Merchant ID",
......@@ -2710,6 +2716,7 @@
"No discount tiers configured. Click \"Add discount tier\" to get started.": "No discount tiers configured. Click \"Add discount tier\" to get started.",
"No duplicate keys found": "No duplicate keys found",
"No enabled tokens available": "No enabled tokens available",
"No encryption": "No encryption",
"No endpoints configured. Switch to JSON mode or add rows to define endpoints.": "No endpoints configured. Switch to JSON mode or add rows to define endpoints.",
"No FAQ entries available": "No FAQ entries available",
"No FAQ entries yet. Click \"Add FAQ\" to create one.": "No FAQ entries yet. Click \"Add FAQ\" to create one.",
......@@ -2724,6 +2731,7 @@
"No history data available": "No history data available",
"No incidents in the last 24 hours": "No incidents in the last 24 hours",
"No incidents in the last 30 days": "No incidents in the last 30 days",
"No instances have reported yet.": "No instances have reported yet.",
"No Inviter": "No Inviter",
"No keys found": "No keys found",
"No latency data available": "No latency data available",
......@@ -2820,10 +2828,11 @@
"Node": "Node",
"Node filters": "Node filters",
"Node Name": "Node Name",
"Node role": "Node role",
"Nodes reporting from this deployment and their latest heartbeat.": "Nodes reporting from this deployment and their latest heartbeat.",
"Non-stream": "Non-stream",
"Non-zero invitation rewards require compliance confirmation in Payment Gateway settings.": "Non-zero invitation rewards require compliance confirmation in Payment Gateway settings.",
"None": "None",
"No encryption": "No encryption",
"noreply@example.com": "noreply@example.com",
"Normalized:": "Normalized:",
"Not available": "Not available",
......@@ -2890,6 +2899,7 @@
"One IP or CIDR range per line": "One IP or CIDR range per line",
"One IP per line (empty for no restriction)": "One IP per line (empty for no restriction)",
"one keyword per line": "one keyword per line",
"online": "online",
"Online": "Online",
"Online payment is not enabled. Please contact the administrator.": "Online payment is not enabled. Please contact the administrator.",
"Online topup is not enabled. Please use redemption code or contact administrator.": "Online topup is not enabled. Please use redemption code or contact administrator.",
......@@ -3599,6 +3609,7 @@
"Resetting...": "Resetting...",
"Resolve Conflicts": "Resolve Conflicts",
"Resource Configuration": "Resource Configuration",
"Resources": "Resources",
"Response": "Response",
"Response Time": "Response Time",
"Response time: {{duration}}": "Response time: {{duration}}",
......@@ -3668,6 +3679,7 @@
"Run tests for the selected models": "Run tests for the selected models",
"running": "running",
"Running": "Running",
"Runtime": "Runtime",
"Runway": "Runway",
"s": "s",
"Safety Settings": "Safety Settings",
......@@ -3946,7 +3958,6 @@
"Slug is required": "Slug is required",
"Slug must be less than 100 characters": "Slug must be less than 100 characters",
"Smallest USD amount users can recharge (Epay)": "Smallest USD amount users can recharge (Epay)",
"SSL/TLS": "SSL/TLS",
"SMTP Email": "SMTP Email",
"SMTP encryption": "SMTP encryption",
"SMTP Host": "SMTP Host",
......@@ -3975,15 +3986,18 @@
"Special usable group rules can add, remove, or append selectable token groups for a specific user group.": "Special usable group rules can add, remove, or append selectable token groups for a specific user group.",
"Spend limited": "Spend limited",
"SQLite stores all data in a single file. Make sure that file is persisted when running in containers.": "SQLite stores all data in a single file. Make sure that file is persisted when running in containers.",
"SSL/TLS": "SSL/TLS",
"SSRF Protection": "SSRF Protection",
"stale": "stale",
"Standard": "Standard",
"Standard price": "Standard price",
"STARTTLS": "STARTTLS",
"Start": "Start",
"Start a conversation to see messages here": "Start a conversation to see messages here",
"Start collecting payments globally without registering a company. Built for indie developers, OPC sole proprietorships, and startups. Waffo Pancake acts as your Merchant of Record, taking on the compliance burden of global payment collection — consumption tax, invoicing, subscription management, refunds, and chargebacks. Solo developers can launch fast and stay focused on product instead of compliance. Onboard in minutes — one prompt to a full integration.": "Start collecting payments globally without registering a company. Built for indie developers, OPC sole proprietorships, and startups. Waffo Pancake acts as your Merchant of Record, taking on the compliance burden of global payment collection — consumption tax, invoicing, subscription management, refunds, and chargebacks. Solo developers can launch fast and stay focused on product instead of compliance. Onboard in minutes — one prompt to a full integration.",
"Start for free with generous limits. No credit card required.": "Start for free with generous limits. No credit card required.",
"Start Time": "Start Time",
"Started": "Started",
"STARTTLS": "STARTTLS",
"Static page describing the platform.": "Static page describing the platform.",
"Statistical count": "Statistical count",
"Statistical quota": "Statistical quota",
......@@ -4006,6 +4020,7 @@
"Stop testing": "Stop testing",
"Stopping batch test...": "Stopping batch test...",
"Stopping...": "Stopping...",
"Storage": "Storage",
"Store": "Store",
"Store + product created": "Store + product created",
"Store ID": "Store ID",
......@@ -4237,6 +4252,7 @@
"This feature is experimental. Configuration format and behavior may change.": "This feature is experimental. Configuration format and behavior may change.",
"This feature requires server-side WeChat configuration": "This feature requires server-side WeChat configuration",
"This identifier is sent to the payment backend when creating an order. Use alipay for Alipay, wxpay for WeChat Pay, stripe for Stripe. Custom values must be supported by your payment provider.": "This identifier is sent to the payment backend when creating an order. Use alipay for Alipay, wxpay for WeChat Pay, stripe for Stripe. Custom values must be supported by your payment provider.",
"This instance is using an automatic hostname. Set NODE_NAME to a stable unique value for multi-instance management.": "This instance is using an automatic hostname. Set NODE_NAME to a stable unique value for multi-instance management.",
"This may cause cache failures.": "This may cause cache failures.",
"This may take a few moments while we validate the request and update your session.": "This may take a few moments while we validate the request and update your session.",
"This model has both fixed price and ratio billing conflicts": "This model has both fixed price and ratio billing conflicts",
......@@ -4561,6 +4577,7 @@
"USD price per 1M tokens.": "USD price per 1M tokens.",
"Use +: to add a group, -: to remove a default selectable group, or no prefix to append a group.": "Use +: to add a group, -: to remove a default selectable group, or no prefix to append a group.",
"Use a compatible browser or device with biometric authentication or a security key to register a Passkey.": "Use a compatible browser or device with biometric authentication or a security key to register a Passkey.",
"Use a different stable value for each instance, then restart the service.": "Use a different stable value for each instance, then restart the service.",
"Use a path to append it to the channel Base URL, or enter a full URL to override the Base URL for this route.": "Use a path to append it to the channel Base URL, or enter a full URL to override the Base URL for this route.",
"Use authenticator code": "Use authenticator code",
"Use backup code": "Use backup code",
......@@ -4663,6 +4680,7 @@
"Verify Setup": "Verify Setup",
"Verify your database connection": "Verify your database connection",
"Verifying credentials and pulling stores from your Pancake account...": "Verifying credentials and pulling stores from your Pancake account...",
"Version": "Version",
"Version Overrides": "Version Overrides",
"Vertex AI": "Vertex AI",
"Vertex AI API Key mode does not support batch creation": "Vertex AI API Key mode does not support batch creation",
......@@ -4734,8 +4752,9 @@
"Warning: Disabling 2FA will make your account less secure.": "Warning: Disabling 2FA will make your account less secure.",
"Warning: This action is permanent and irreversible!": "Warning: This action is permanent and irreversible!",
"We apologize for the inconvenience.": "We apologize for the inconvenience.",
"We could not load the setup status.": "We could not load the setup status.",
"We could not load instances.": "We could not load instances.",
"We could not load system tasks.": "We could not load system tasks.",
"We could not load the setup status.": "We could not load the setup status.",
"We will prompt your device to confirm using biometrics or your hardware key.": "We will prompt your device to confirm using biometrics or your hardware key.",
"We'll be back online shortly.": "We'll be back online shortly.",
"Web search": "Web search",
......@@ -4798,6 +4817,7 @@
"with the API key from your token settings.": "with the API key from your token settings.",
"Without additional conditions, only the type above is used for pruning.": "Without additional conditions, only the type above is used for pruning.",
"Worker Access Key": "Worker Access Key",
"Worker instances do not run master-only background tasks.": "Worker instances do not run master-only background tasks.",
"Worker Proxy": "Worker Proxy",
"Worker URL": "Worker URL",
"Workspaces": "Workspaces",
......
......@@ -915,6 +915,7 @@
"Configure keyword filtering for prompts and responses.": "Configurer le filtrage par mots-clés pour les invites et les réponses.",
"Configure model, caching, and group ratios used for billing": "Configurer les ratios de modèle, de mise en cache et de groupe utilisés pour la facturation",
"Configure monitoring status page groups for the dashboard": "Configurer les groupes de pages d'état de surveillance pour le tableau de bord",
"Configure NODE_NAME": "Configurer NODE_NAME",
"Configure per-model ratio for image inputs or outputs.": "Configurer le ratio par modèle pour les entrées ou sorties d'images.",
"Configure per-tool unit prices ($/1K calls). Per-request models do not incur additional tool fees.": "Définissez le prix unitaire de chaque outil ($/1K appels). Les modèles facturés à la requête n'entraînent pas de frais d'outils supplémentaires.",
"Configure pricing ratios for a specific model.": "Configurer les ratios de tarification pour un modèle spécifique.",
......@@ -1052,6 +1053,7 @@
"Cost Tracking": "Suivi des coûts",
"Count must be between {{min}} and {{max}}": "Le nombre doit être compris entre {{min}} et {{max}}",
"Coze": "Coze",
"CPU": "Processeur",
"CPU Threshold (%)": "Seuil CPU (%)",
"Create": "Créer",
"Create a copy of:": "Créer une copie de :",
......@@ -1935,6 +1937,7 @@
"Format: AppId|SecretId|SecretKey": "Format : AppId|SecretId|SecretKey",
"Forward requests directly to upstream providers without any post-processing.": "Transférer les requêtes directement aux fournisseurs amont sans aucun post-traitement.",
"Frames per second": "Images par seconde",
"Free": "Libre",
"Free: {{free}} / Total: {{total}}": "Disponible : {{free}} / Total : {{total}}",
"Friendly name to identify this channel": "Nom convivial pour identifier ce canal",
"From Address": "De l'adresse",
......@@ -2186,6 +2189,7 @@
"Inspect requests, errors, and billing details": "Inspecter les requêtes, les erreurs et les détails de facturation",
"Inspect user prompts": "Inspecter les invites utilisateur",
"Instance": "Instance",
"Instances": "Instances",
"Insufficient balance": "Solde insuffisant",
"Integrations": "Intégrations",
"Inter-group overrides": "Dérogations inter-groupes",
......@@ -2287,7 +2291,7 @@
"Last check time": "Dernière vérification",
"Last detected addable models": "Derniers modèles ajoutables détectés",
"Last Login": "Dernière connexion",
"Last Seen": "Dernière fois",
"Last Seen": "Dernier signal",
"Last Tested": "Dernier testé",
"Last updated:": "Dernière mise à jour :",
"Last Used": "Dernière utilisation",
......@@ -2411,6 +2415,7 @@
"Map upstream status codes to different codes": "Mapper les codes de statut amont à différents codes",
"Market Share": "Part de marché",
"Marketing": "Marketing",
"Master instances run scheduled background tasks.": "Les instances master exécutent les tâches planifiées en arrière-plan.",
"Match All (AND)": "Toutes (AND)",
"Match Any (OR)": "N'importe laquelle (OR)",
"Match Mode": "Mode de correspondance",
......@@ -2449,6 +2454,7 @@
"Media pricing": "Tarification multimédia",
"Median time-to-first-token (TTFT) sampled hourly per group": "Latence médiane jusqu'au premier jeton (TTFT) échantillonnée par heure et par groupe",
"Medical Q&A, mental health support": "Q&R médicales, soutien en santé mentale",
"Memory": "Mémoire",
"Memory Hits": "Hits mémoire",
"Memory Threshold (%)": "Seuil mémoire (%)",
"Merchant ID": "ID du commerçant",
......@@ -2710,6 +2716,7 @@
"No discount tiers configured. Click \"Add discount tier\" to get started.": "Aucun niveau de réduction configuré. Cliquez sur « Ajouter un niveau de réduction » pour commencer.",
"No duplicate keys found": "Aucune clé dupliquée trouvée",
"No enabled tokens available": "Aucun token activé disponible",
"No encryption": "Aucun chiffrement",
"No endpoints configured. Switch to JSON mode or add rows to define endpoints.": "Aucun point de terminaison configuré. Passez en mode JSON ou ajoutez des lignes pour définir les points de terminaison.",
"No FAQ entries available": "Aucune entrée FAQ disponible",
"No FAQ entries yet. Click \"Add FAQ\" to create one.": "Aucune entrée FAQ pour l'instant. Cliquez sur \"Ajouter une FAQ\" pour en créer une.",
......@@ -2724,6 +2731,7 @@
"No history data available": "Aucune donnée historique disponible",
"No incidents in the last 24 hours": "Aucun incident au cours des dernières 24 heures",
"No incidents in the last 30 days": "Aucun incident sur les 30 derniers jours",
"No instances have reported yet.": "Aucune instance ne s’est encore signalée.",
"No Inviter": "Pas d'inviteur",
"No keys found": "Aucune clé trouvée",
"No latency data available": "Aucune donnée de latence disponible",
......@@ -2820,10 +2828,11 @@
"Node": "Nœud",
"Node filters": "Filtres de nœuds",
"Node Name": "Nom du nœud",
"Node role": "Rôle du nœud",
"Nodes reporting from this deployment and their latest heartbeat.": "Nœuds signalés par ce déploiement et leur dernier battement de cœur.",
"Non-stream": "Non-streaming",
"Non-zero invitation rewards require compliance confirmation in Payment Gateway settings.": "Les récompenses d’invitation non nulles nécessitent une confirmation de conformité dans les paramètres de la passerelle de paiement.",
"None": "Aucun",
"No encryption": "Aucun chiffrement",
"noreply@example.com": "noreply@example.com",
"Normalized:": "Normalisé :",
"Not available": "Non disponible",
......@@ -2890,6 +2899,7 @@
"One IP or CIDR range per line": "Une IP ou plage CIDR par ligne",
"One IP per line (empty for no restriction)": "Une IP par ligne (laisser vide pour aucune restriction)",
"one keyword per line": "un mot-clé par ligne",
"online": "en ligne",
"Online": "En ligne",
"Online payment is not enabled. Please contact the administrator.": "Le paiement en ligne n'est pas activé. Veuillez contacter l'administrateur.",
"Online topup is not enabled. Please use redemption code or contact administrator.": "La recharge en ligne n'est pas activée. Veuillez utiliser un code d'échange ou contacter l'administrateur.",
......@@ -3599,6 +3609,7 @@
"Resetting...": "Réinitialisation...",
"Resolve Conflicts": "Résoudre les conflits",
"Resource Configuration": "Configuration des ressources",
"Resources": "Ressources",
"Response": "Réponse",
"Response Time": "Temps de réponse",
"Response time: {{duration}}": "Temps de réponse : {{duration}}",
......@@ -3668,6 +3679,7 @@
"Run tests for the selected models": "Exécuter les tests pour les modèles sélectionnés",
"running": "en cours",
"Running": "En cours",
"Runtime": "Environnement",
"Runway": "Durée restante",
"s": "s",
"Safety Settings": "Paramètres de sécurité",
......@@ -3946,7 +3958,6 @@
"Slug is required": "Le slug est requis",
"Slug must be less than 100 characters": "Le slug doit contenir moins de 100 caractères",
"Smallest USD amount users can recharge (Epay)": "Montant minimum en USD que les utilisateurs peuvent recharger (Epay)",
"SSL/TLS": "SSL/TLS",
"SMTP Email": "E-mail SMTP",
"SMTP encryption": "Chiffrement SMTP",
"SMTP Host": "Hôte SMTP",
......@@ -3975,15 +3986,18 @@
"Special usable group rules can add, remove, or append selectable token groups for a specific user group.": "Les règles de groupes utilisables spéciaux peuvent ajouter, supprimer ou annexer des groupes de jetons sélectionnables pour un groupe utilisateur précis.",
"Spend limited": "Dépenses limitées",
"SQLite stores all data in a single file. Make sure that file is persisted when running in containers.": "SQLite stocke toutes les données dans un seul fichier. Assurez-vous que ce fichier est persisté lors de l'exécution dans des conteneurs.",
"SSL/TLS": "SSL/TLS",
"SSRF Protection": "Protection SSRF",
"stale": "expiré",
"Standard": "Standard",
"Standard price": "Prix standard",
"STARTTLS": "STARTTLS",
"Start": "Début",
"Start a conversation to see messages here": "Démarrez une conversation pour voir les messages ici",
"Start collecting payments globally without registering a company. Built for indie developers, OPC sole proprietorships, and startups. Waffo Pancake acts as your Merchant of Record, taking on the compliance burden of global payment collection — consumption tax, invoicing, subscription management, refunds, and chargebacks. Solo developers can launch fast and stay focused on product instead of compliance. Onboard in minutes — one prompt to a full integration.": "Commencez à encaisser des paiements dans le monde entier sans créer de société. Conçu pour les développeurs indépendants, les entrepreneurs individuels OPC et les startups. Waffo Pancake agit comme Merchant of Record et prend en charge la conformité liée à l’encaissement mondial : taxes à la consommation, facturation, gestion des abonnements, remboursements et rétrofacturations. Les développeurs solo peuvent lancer rapidement leur produit et rester concentrés sur celui-ci plutôt que sur la conformité. Intégration en quelques minutes, d’une seule invite à une intégration complète.",
"Start for free with generous limits. No credit card required.": "Commencez gratuitement avec des limites généreuses. Aucune carte de crédit requise.",
"Start Time": "Heure de début",
"Started": "Démarré",
"STARTTLS": "STARTTLS",
"Static page describing the platform.": "Page statique décrivant la plateforme.",
"Statistical count": "Nombre statistique",
"Statistical quota": "Quota statistique",
......@@ -4006,6 +4020,7 @@
"Stop testing": "Arrêter le test",
"Stopping batch test...": "Arrêt du test par lots...",
"Stopping...": "Arrêt...",
"Storage": "Stockage",
"Store": "Store",
"Store + product created": "Boutique + produit créés",
"Store ID": "ID du magasin",
......@@ -4237,6 +4252,7 @@
"This feature is experimental. Configuration format and behavior may change.": "Cette fonctionnalité est expérimentale. Le format de configuration et le comportement peuvent changer.",
"This feature requires server-side WeChat configuration": "Cette fonctionnalité nécessite une configuration WeChat côté serveur",
"This identifier is sent to the payment backend when creating an order. Use alipay for Alipay, wxpay for WeChat Pay, stripe for Stripe. Custom values must be supported by your payment provider.": "Cet identifiant est envoyé au backend de paiement lors de la création d’une commande. Utilisez alipay pour Alipay, wxpay pour WeChat Pay, stripe pour Stripe. Les valeurs personnalisées doivent être prises en charge par votre fournisseur de paiement.",
"This instance is using an automatic hostname. Set NODE_NAME to a stable unique value for multi-instance management.": "Cette instance utilise un nom d’hôte automatique. Définissez NODE_NAME sur une valeur stable et unique pour la gestion multi-instance.",
"This may cause cache failures.": "Cela peut provoquer des échecs de cache.",
"This may take a few moments while we validate the request and update your session.": "Cela peut prendre quelques instants pendant que nous validons la requête et mettons à jour votre session.",
"This model has both fixed price and ratio billing conflicts": "Ce modèle présente des conflits de facturation à la fois en prix fixe et au ratio",
......@@ -4561,6 +4577,7 @@
"USD price per 1M tokens.": "Prix en USD par million de tokens.",
"Use +: to add a group, -: to remove a default selectable group, or no prefix to append a group.": "Utilisez +: pour ajouter un groupe, -: pour supprimer un groupe sélectionnable par défaut, ou aucun préfixe pour annexer un groupe.",
"Use a compatible browser or device with biometric authentication or a security key to register a Passkey.": "Utilisez un navigateur ou un appareil compatible avec l'authentification biométrique ou une clé de sécurité pour enregistrer une clé d'accès (Passkey).",
"Use a different stable value for each instance, then restart the service.": "Utilisez une valeur stable différente pour chaque instance, puis redémarrez le service.",
"Use a path to append it to the channel Base URL, or enter a full URL to override the Base URL for this route.": "Utilisez un chemin pour l’ajouter à la Base URL du canal, ou saisissez une URL complète pour remplacer la Base URL pour cette route.",
"Use authenticator code": "Utiliser le code de l'authentificateur",
"Use backup code": "Utiliser un code de secours",
......@@ -4663,6 +4680,7 @@
"Verify Setup": "Vérifier la configuration",
"Verify your database connection": "Vérifiez votre connexion à la base de données",
"Verifying credentials and pulling stores from your Pancake account...": "Vérification des identifiants et récupération des boutiques depuis votre compte Pancake...",
"Version": "Version",
"Version Overrides": "Remplacements de version",
"Vertex AI": "Vertex AI",
"Vertex AI API Key mode does not support batch creation": "Le mode clé API Vertex AI ne prend pas en charge la création par lot",
......@@ -4734,8 +4752,9 @@
"Warning: Disabling 2FA will make your account less secure.": "Avertissement : La désactivation de la 2FA rendra votre compte moins sécurisé.",
"Warning: This action is permanent and irreversible!": "Avertissement : Cette action est permanente et irréversible !",
"We apologize for the inconvenience.": "Nous nous excusons pour le désagrément.",
"We could not load the setup status.": "Nous n'avons pas pu charger l'état de la configuration.",
"We could not load instances.": "Impossible de charger les instances.",
"We could not load system tasks.": "Impossible de charger les tâches système.",
"We could not load the setup status.": "Nous n'avons pas pu charger l'état de la configuration.",
"We will prompt your device to confirm using biometrics or your hardware key.": "Nous allons demander à votre appareil de confirmer en utilisant la biométrie ou votre clé matérielle.",
"We'll be back online shortly.": "Nous serons de retour en ligne sous peu.",
"Web search": "Recherche web",
......@@ -4798,6 +4817,7 @@
"with the API key from your token settings.": "par la clé API de votre page de jetons.",
"Without additional conditions, only the type above is used for pruning.": "Sans conditions supplémentaires, seul le type ci-dessus est utilisé pour le nettoyage.",
"Worker Access Key": "Clé d'accès du Worker",
"Worker instances do not run master-only background tasks.": "Les instances worker n’exécutent pas les tâches d’arrière-plan réservées au master.",
"Worker Proxy": "Proxy Worker",
"Worker URL": "URL du Worker",
"Workspaces": "Espaces de travail",
......
......@@ -915,6 +915,7 @@
"Configure keyword filtering for prompts and responses.": "プロンプトと応答のキーワードフィルタリングを設定します。",
"Configure model, caching, and group ratios used for billing": "請求に使用されるモデル、キャッシュ、およびグループ比率を設定します。",
"Configure monitoring status page groups for the dashboard": "ダッシュボードの監視ステータスページグループを設定します。",
"Configure NODE_NAME": "NODE_NAME を設定",
"Configure per-model ratio for image inputs or outputs.": "画像の入力または出力のモデルごとの比率を設定します。",
"Configure per-tool unit prices ($/1K calls). Per-request models do not incur additional tool fees.": "ツールごとの単価($/1K 回)を設定します。リクエスト課金モデルでは追加工具料金はかかりません。",
"Configure pricing ratios for a specific model.": "特定のモデルの料金比率を設定します。",
......@@ -1052,6 +1053,7 @@
"Cost Tracking": "コスト追跡",
"Count must be between {{min}} and {{max}}": "カウントは{{min}}から{{max}}の間である必要があります",
"Coze": "Coze",
"CPU": "CPU",
"CPU Threshold (%)": "CPU 閾値 (%)",
"Create": "新規作成",
"Create a copy of:": "コピーを作成:",
......@@ -1935,6 +1937,7 @@
"Format: AppId|SecretId|SecretKey": "形式: AppId|SecretId|SecretKey",
"Forward requests directly to upstream providers without any post-processing.": "ポストプロセスなしで、リクエストをアップストリームプロバイダーに直接転送します。",
"Frames per second": "フレームレート",
"Free": "空き",
"Free: {{free}} / Total: {{total}}": "空き容量: {{free}} / 合計: {{total}}",
"Friendly name to identify this channel": "このチャネルを識別するための表示名",
"From Address": "差出人アドレス",
......@@ -2186,6 +2189,7 @@
"Inspect requests, errors, and billing details": "リクエスト、エラー、請求詳細を確認",
"Inspect user prompts": "ユーザープロンプトの検査",
"Instance": "インスタンス",
"Instances": "インスタンス",
"Insufficient balance": "残高が不足しています",
"Integrations": "統合",
"Inter-group overrides": "グループ間上書き",
......@@ -2287,7 +2291,7 @@
"Last check time": "最終チェック時刻",
"Last detected addable models": "最後に検出された追加可能モデル",
"Last Login": "最終ログイン",
"Last Seen": "最終確認",
"Last Seen": "最終報告",
"Last Tested": "最終テスト日時",
"Last updated:": "最終更新日:",
"Last Used": "最終使用",
......@@ -2411,6 +2415,7 @@
"Map upstream status codes to different codes": "アップストリームのステータスコードを別のコードにマッピングする",
"Market Share": "マーケットシェア",
"Marketing": "マーケティング",
"Master instances run scheduled background tasks.": "master インスタンスはスケジュールされたバックグラウンドタスクを実行します。",
"Match All (AND)": "すべて一致(AND)",
"Match Any (OR)": "いずれか一致(OR)",
"Match Mode": "マッチモード",
......@@ -2449,6 +2454,7 @@
"Media pricing": "メディア料金",
"Median time-to-first-token (TTFT) sampled hourly per group": "グループ別に毎時サンプリングした最初のトークンまでの中央値レイテンシ (TTFT)",
"Medical Q&A, mental health support": "医療Q&A・メンタルヘルスサポート",
"Memory": "メモリ",
"Memory Hits": "メモリヒット",
"Memory Threshold (%)": "メモリ閾値 (%)",
"Merchant ID": "マーチャントID",
......@@ -2710,6 +2716,7 @@
"No discount tiers configured. Click \"Add discount tier\" to get started.": "割引ティアは設定されていません。「割引ティアを追加」をクリックして開始してください。",
"No duplicate keys found": "重複キーが見つかりませんでした",
"No enabled tokens available": "有効なトークンがありません",
"No encryption": "暗号化なし",
"No endpoints configured. Switch to JSON mode or add rows to define endpoints.": "エンドポイントが設定されていません。JSONモードに切り替えるか、エンドポイントを定義するために行を追加してください。",
"No FAQ entries available": "FAQエントリがありません",
"No FAQ entries yet. Click \"Add FAQ\" to create one.": "FAQエントリはまだありません。「FAQを追加」をクリックして作成してください。",
......@@ -2724,6 +2731,7 @@
"No history data available": "履歴データがありません",
"No incidents in the last 24 hours": "過去 24 時間にインシデントはありません",
"No incidents in the last 30 days": "過去 30 日間でインシデントはありません",
"No instances have reported yet.": "まだ報告されたインスタンスはありません。",
"No Inviter": "招待者なし",
"No keys found": "キーが見つかりません",
"No latency data available": "レイテンシデータがありません",
......@@ -2820,10 +2828,11 @@
"Node": "ノード",
"Node filters": "ノードフィルター",
"Node Name": "ノード名",
"Node role": "ノードの役割",
"Nodes reporting from this deployment and their latest heartbeat.": "このデプロイから報告されたノードと最新のハートビート。",
"Non-stream": "非ストリーミング",
"Non-zero invitation rewards require compliance confirmation in Payment Gateway settings.": "0 以外の招待報酬には、支払いゲートウェイ設定でのコンプライアンス確認が必要です。",
"None": "なし",
"No encryption": "暗号化なし",
"noreply@example.com": "noreply@example.com",
"Normalized:": "正規化:",
"Not available": "利用できません",
......@@ -2890,6 +2899,7 @@
"One IP or CIDR range per line": "1行に1つのIPまたはCIDR範囲",
"One IP per line (empty for no restriction)": "1行に1つのIP (制限なしの場合は空欄)",
"one keyword per line": "1行に1つのキーワード",
"online": "オンライン",
"Online": "オンライン",
"Online payment is not enabled. Please contact the administrator.": "オンライン決済が有効になっていません。管理者にお問い合わせください。",
"Online topup is not enabled. Please use redemption code or contact administrator.": "オンラインチャージは有効になっていません。引き換えコードを使用するか、管理者に連絡してください。",
......@@ -3599,6 +3609,7 @@
"Resetting...": "リセット中...",
"Resolve Conflicts": "競合を解決",
"Resource Configuration": "リソース設定",
"Resources": "リソース",
"Response": "レスポンス",
"Response Time": "応答時間",
"Response time: {{duration}}": "応答時間: {{duration}}",
......@@ -3668,6 +3679,7 @@
"Run tests for the selected models": "選択したモデルのテストを実行",
"running": "実行中",
"Running": "実行中",
"Runtime": "実行環境",
"Runway": "残り期間",
"s": "s",
"Safety Settings": "安全設定",
......@@ -3946,7 +3958,6 @@
"Slug is required": "スラッグは必須です",
"Slug must be less than 100 characters": "スラッグは100文字以内にしてください",
"Smallest USD amount users can recharge (Epay)": "ユーザーがチャージできる最小USD金額 (Epay)",
"SSL/TLS": "SSL/TLS",
"SMTP Email": "SMTPメール",
"SMTP encryption": "SMTP 暗号化方式",
"SMTP Host": "SMTPホスト",
......@@ -3975,15 +3986,18 @@
"Special usable group rules can add, remove, or append selectable token groups for a specific user group.": "特殊利用可能グループルールでは、特定ユーザーグループ向けに選択可能なトークングループを追加、削除、追記できます。",
"Spend limited": "支出制限中",
"SQLite stores all data in a single file. Make sure that file is persisted when running in containers.": "SQLite はすべてのデータを単一ファイルに保存します。コンテナで実行する場合は、ファイルが永続化されていることを確認してください。",
"SSL/TLS": "SSL/TLS",
"SSRF Protection": "SSRF保護",
"stale": "期限切れ",
"Standard": "標準",
"Standard price": "標準価格",
"STARTTLS": "STARTTLS",
"Start": "開始",
"Start a conversation to see messages here": "会話を開始すると、ここにメッセージが表示されます",
"Start collecting payments globally without registering a company. Built for indie developers, OPC sole proprietorships, and startups. Waffo Pancake acts as your Merchant of Record, taking on the compliance burden of global payment collection — consumption tax, invoicing, subscription management, refunds, and chargebacks. Solo developers can launch fast and stay focused on product instead of compliance. Onboard in minutes — one prompt to a full integration.": "法人を設立せずに世界中で決済を受け付けられます。個人開発者、OPC 個人事業主、スタートアップ向けに設計されています。Waffo Pancake は Merchant of Record として、消費税、請求書、サブスクリプション管理、返金、チャージバックなど、グローバル決済のコンプライアンス負担を引き受けます。個人開発者はコンプライアンスではなくプロダクトに集中しながら素早くローンチできます。数分でオンボーディングし、1 つのプロンプトから完全な統合まで進められます。",
"Start for free with generous limits. No credit card required.": "豊富な無料枠で始められます。クレジットカードは不要です。",
"Start Time": "開始時間",
"Started": "起動時刻",
"STARTTLS": "STARTTLS",
"Static page describing the platform.": "プラットフォームを説明する静的ページ。",
"Statistical count": "統計数",
"Statistical quota": "統計クォータ",
......@@ -4006,6 +4020,7 @@
"Stop testing": "テストを停止",
"Stopping batch test...": "バッチテストを停止中...",
"Stopping...": "停止中...",
"Storage": "ストレージ",
"Store": "Store",
"Store + product created": "ストア + 商品を作成しました",
"Store ID": "ストア ID",
......@@ -4237,6 +4252,7 @@
"This feature is experimental. Configuration format and behavior may change.": "この機能は実験的です。設定フォーマットや動作は変更される可能性があります。",
"This feature requires server-side WeChat configuration": "この機能にはサーバー側のWeChat設定が必要です",
"This identifier is sent to the payment backend when creating an order. Use alipay for Alipay, wxpay for WeChat Pay, stripe for Stripe. Custom values must be supported by your payment provider.": "注文作成時に、この識別子が決済バックエンドへ送信されます。Alipay は alipay、WeChat Pay は wxpay、Stripe は stripe を使ってください。カスタム値は決済サービス側で対応している必要があります。",
"This instance is using an automatic hostname. Set NODE_NAME to a stable unique value for multi-instance management.": "このインスタンスは自動ホスト名を使用しています。マルチインスタンス管理のために、安定した一意の NODE_NAME を設定してください。",
"This may cause cache failures.": "これによりキャッシュ障害が発生する可能性があります。",
"This may take a few moments while we validate the request and update your session.": "リクエストを検証し、セッションを更新するのに数分かかる場合があります。",
"This model has both fixed price and ratio billing conflicts": "このモデルには固定価格と比率請求の両方の競合があります",
......@@ -4561,6 +4577,7 @@
"USD price per 1M tokens.": "100万トークンあたりのUSD価格。",
"Use +: to add a group, -: to remove a default selectable group, or no prefix to append a group.": "+: はグループ追加、-: はデフォルト選択可能グループの削除、接頭辞なしはグループ追記に使います。",
"Use a compatible browser or device with biometric authentication or a security key to register a Passkey.": "生体認証またはセキュリティキーを備えた互換性のあるブラウザまたはデバイスを使用して、パスキーを登録してください。",
"Use a different stable value for each instance, then restart the service.": "インスタンスごとに異なる安定した値を使用し、その後サービスを再起動してください。",
"Use a path to append it to the channel Base URL, or enter a full URL to override the Base URL for this route.": "パスを入力するとチャネルの Base URL に追加されます。完全な URL を入力すると、このルートでは Base URL を使わずその URL を使用します。",
"Use authenticator code": "認証コードを使用",
"Use backup code": "バックアップコードを使用",
......@@ -4663,6 +4680,7 @@
"Verify Setup": "設定を確認",
"Verify your database connection": "データベース接続を確認",
"Verifying credentials and pulling stores from your Pancake account...": "認証情報を検証し、Pancake アカウントからストアを取得しています...",
"Version": "バージョン",
"Version Overrides": "バージョンオーバーライド",
"Vertex AI": "Vertex AI",
"Vertex AI API Key mode does not support batch creation": "Vertex AI API Key モードは一括作成をサポートしていません",
......@@ -4734,8 +4752,9 @@
"Warning: Disabling 2FA will make your account less secure.": "警告: 2FAを無効にすると、アカウントのセキュリティが低下します。",
"Warning: This action is permanent and irreversible!": "警告: この操作は永続的で元に戻せません!",
"We apologize for the inconvenience.": "ご不便をおかけして申し訳ありません。",
"We could not load the setup status.": "セットアップステータスを読み込めませんでした。",
"We could not load instances.": "インスタンス情報を読み込めませんでした。",
"We could not load system tasks.": "システムタスクを読み込めませんでした。",
"We could not load the setup status.": "セットアップステータスを読み込めませんでした。",
"We will prompt your device to confirm using biometrics or your hardware key.": "生体認証またはハードウェアキーを使用して確認するよう、デバイスにプロンプトが表示されます。",
"We'll be back online shortly.": "まもなくオンラインに戻ります。",
"Web search": "ウェブ検索",
......@@ -4798,6 +4817,7 @@
"with the API key from your token settings.": "をトークン設定の API キーに置き換えてください。",
"Without additional conditions, only the type above is used for pruning.": "追加条件がない場合、上記のtypeのみが削除に使用されます。",
"Worker Access Key": "Workerアクセスキー",
"Worker instances do not run master-only background tasks.": "worker インスタンスは master 専用のバックグラウンドタスクを実行しません。",
"Worker Proxy": "Workerプロキシ",
"Worker URL": "ワーカーURL",
"Workspaces": "ワークスペース",
......
......@@ -915,6 +915,7 @@
"Configure keyword filtering for prompts and responses.": "Настроить фильтрацию по ключевым словам для запросов и ответов.",
"Configure model, caching, and group ratios used for billing": "Настроить модель, кэширование и групповые коэффициенты, используемые для выставления счетов",
"Configure monitoring status page groups for the dashboard": "Настроить группы страниц состояния мониторинга для панели управления",
"Configure NODE_NAME": "Настроить NODE_NAME",
"Configure per-model ratio for image inputs or outputs.": "Настроить коэффициент для каждой модели для ввода или вывода изображений.",
"Configure per-tool unit prices ($/1K calls). Per-request models do not incur additional tool fees.": "Настройте стоимость единицы на инструмент ($/1K вызовов). Для моделей с оплатой за запрос доп. плата за инструменты не взимается.",
"Configure pricing ratios for a specific model.": "Настроить коэффициенты ценообразования для конкретной модели.",
......@@ -1052,6 +1053,7 @@
"Cost Tracking": "Отслеживание затрат",
"Count must be between {{min}} and {{max}}": "Количество должно быть от {{min}} до {{max}}",
"Coze": "Coze",
"CPU": "ЦП",
"CPU Threshold (%)": "Порог CPU (%)",
"Create": "Создать",
"Create a copy of:": "Создать копию:",
......@@ -1935,6 +1937,7 @@
"Format: AppId|SecretId|SecretKey": "Формат: AppId|SecretId|SecretKey",
"Forward requests directly to upstream providers without any post-processing.": "Перенаправлять запросы напрямую upstream-провайдерам без какой-либо постобработки.",
"Frames per second": "Кадров в секунду",
"Free": "Свободно",
"Free: {{free}} / Total: {{total}}": "Свободно: {{free}} / Всего: {{total}}",
"Friendly name to identify this channel": "Дружественное имя для идентификации этого канала",
"From Address": "Отправитель",
......@@ -2186,6 +2189,7 @@
"Inspect requests, errors, and billing details": "Проверяйте запросы, ошибки и детали оплаты",
"Inspect user prompts": "Просмотр запросов пользователя",
"Instance": "Экземпляр",
"Instances": "Экземпляры",
"Insufficient balance": "Недостаточно средств",
"Integrations": "Интеграции",
"Inter-group overrides": "Переопределения между группами",
......@@ -2287,7 +2291,7 @@
"Last check time": "Время последней проверки",
"Last detected addable models": "Последние обнаруженные модели для добавления",
"Last Login": "Последний вход",
"Last Seen": "Последний раз",
"Last Seen": "Последний сигнал",
"Last Tested": "Последняя проверка",
"Last updated:": "Последнее обновление:",
"Last Used": "Последнее использование",
......@@ -2411,6 +2415,7 @@
"Map upstream status codes to different codes": "Сопоставить коды статуса вышестоящего сервера с различными кодами",
"Market Share": "Доля рынка",
"Marketing": "Маркетинг",
"Master instances run scheduled background tasks.": "Экземпляры master выполняют плановые фоновые задачи.",
"Match All (AND)": "Все совпадения (AND)",
"Match Any (OR)": "Любое совпадение (OR)",
"Match Mode": "Режим сопоставления",
......@@ -2449,6 +2454,7 @@
"Media pricing": "Цены для медиа",
"Median time-to-first-token (TTFT) sampled hourly per group": "Медианная задержка первого токена (TTFT), измеряемая ежечасно по группам",
"Medical Q&A, mental health support": "Медицинские Q&A, поддержка ментального здоровья",
"Memory": "Память",
"Memory Hits": "Попаданий памяти",
"Memory Threshold (%)": "Порог памяти (%)",
"Merchant ID": "ID мерчанта",
......@@ -2710,6 +2716,7 @@
"No discount tiers configured. Click \"Add discount tier\" to get started.": "Не настроены уровни скидок. Нажмите \"Добавить уровень скидки\", чтобы начать.",
"No duplicate keys found": "Дубликаты ключей не найдены",
"No enabled tokens available": "Нет доступных активных токенов",
"No encryption": "Без шифрования",
"No endpoints configured. Switch to JSON mode or add rows to define endpoints.": "Конечные точки не настроены. Переключитесь в режим JSON или добавьте строки для определения конечных точек.",
"No FAQ entries available": "Нет доступных записей FAQ",
"No FAQ entries yet. Click \"Add FAQ\" to create one.": "Пока нет записей FAQ. Нажмите \"Добавить FAQ\", чтобы создать одну.",
......@@ -2724,6 +2731,7 @@
"No history data available": "Исторические данные недоступны",
"No incidents in the last 24 hours": "За последние 24 часа инцидентов не было",
"No incidents in the last 30 days": "За последние 30 дней инцидентов не было",
"No instances have reported yet.": "Экземпляры еще не отправляли данные.",
"No Inviter": "Нет пригласившего",
"No keys found": "Ключи не найдены",
"No latency data available": "Данные о задержке недоступны",
......@@ -2820,10 +2828,11 @@
"Node": "Узел",
"Node filters": "Фильтры узлов",
"Node Name": "Имя узла",
"Node role": "Роль узла",
"Nodes reporting from this deployment and their latest heartbeat.": "Узлы этого развертывания и их последнее сердцебиение.",
"Non-stream": "Не потоковый",
"Non-zero invitation rewards require compliance confirmation in Payment Gateway settings.": "Ненулевые награды за приглашения требуют подтверждения соответствия в настройках платежного шлюза.",
"None": "Нет",
"No encryption": "Без шифрования",
"noreply@example.com": "noreply@example.com",
"Normalized:": "Нормализовано:",
"Not available": "Недоступно",
......@@ -2890,6 +2899,7 @@
"One IP or CIDR range per line": "Один IP или диапазон CIDR на строку",
"One IP per line (empty for no restriction)": "Один IP на строку (пусто для отсутствия ограничений)",
"one keyword per line": "одно ключевое слово на строку",
"online": "онлайн",
"Online": "Онлайн",
"Online payment is not enabled. Please contact the administrator.": "Онлайн-оплата не включена. Пожалуйста, свяжитесь с администратором.",
"Online topup is not enabled. Please use redemption code or contact administrator.": "Онлайн-пополнение не включено. Пожалуйста, используйте код активации или свяжитесь с администратором.",
......@@ -3599,6 +3609,7 @@
"Resetting...": "Сброс...",
"Resolve Conflicts": "Разрешить конфликты",
"Resource Configuration": "Конфигурация ресурсов",
"Resources": "Ресурсы",
"Response": "Ответ",
"Response Time": "Время ответа",
"Response time: {{duration}}": "Время ответа: {{duration}}",
......@@ -3668,6 +3679,7 @@
"Run tests for the selected models": "Запустить тесты для выбранных моделей",
"running": "выполняется",
"Running": "Выполняется",
"Runtime": "Среда выполнения",
"Runway": "Запас",
"s": "s",
"Safety Settings": "Настройки безопасности",
......@@ -3946,7 +3958,6 @@
"Slug is required": "Slug обязателен",
"Slug must be less than 100 characters": "Slug должен содержать менее 100 символов",
"Smallest USD amount users can recharge (Epay)": "Минимальная сумма в USD, которую пользователи могут пополнить (Epay)",
"SSL/TLS": "SSL/TLS",
"SMTP Email": "Электронная почта SMTP",
"SMTP encryption": "Шифрование SMTP",
"SMTP Host": "Хост SMTP",
......@@ -3975,15 +3986,18 @@
"Special usable group rules can add, remove, or append selectable token groups for a specific user group.": "Правила специальных доступных групп могут добавлять, удалять или дополнять выбираемые группы токенов для конкретной группы пользователей.",
"Spend limited": "Ограничение расходов",
"SQLite stores all data in a single file. Make sure that file is persisted when running in containers.": "SQLite хранит все данные в одном файле. Убедитесь, что файл сохраняется при работе в контейнерах.",
"SSL/TLS": "SSL/TLS",
"SSRF Protection": "Защита от SSRF",
"stale": "устарел",
"Standard": "Стандартный",
"Standard price": "Стандартная цена",
"STARTTLS": "STARTTLS",
"Start": "Начало",
"Start a conversation to see messages here": "Начните разговор, чтобы увидеть сообщения здесь",
"Start collecting payments globally without registering a company. Built for indie developers, OPC sole proprietorships, and startups. Waffo Pancake acts as your Merchant of Record, taking on the compliance burden of global payment collection — consumption tax, invoicing, subscription management, refunds, and chargebacks. Solo developers can launch fast and stay focused on product instead of compliance. Onboard in minutes — one prompt to a full integration.": "Начните принимать платежи по всему миру без регистрации компании. Подходит для независимых разработчиков, индивидуальных предпринимателей OPC и стартапов. Waffo Pancake выступает как Merchant of Record и берет на себя комплаенс глобального приема платежей: потребительские налоги, выставление счетов, управление подписками, возвраты и чарджбеки. Одиночные разработчики могут быстро запуститься и сосредоточиться на продукте, а не на комплаенсе. Подключение за минуты — от одного запроса до полной интеграции.",
"Start for free with generous limits. No credit card required.": "Начните бесплатно с щедрыми лимитами. Кредитная карта не требуется.",
"Start Time": "Время начала",
"Started": "Запущен",
"STARTTLS": "STARTTLS",
"Static page describing the platform.": "Статическая страница, описывающая платформу.",
"Statistical count": "Статистический подсчет",
"Statistical quota": "Статистическая квота",
......@@ -4006,6 +4020,7 @@
"Stop testing": "Остановить тестирование",
"Stopping batch test...": "Остановка пакетного теста...",
"Stopping...": "Остановка...",
"Storage": "Хранилище",
"Store": "Store",
"Store + product created": "Магазин и продукт созданы",
"Store ID": "ID магазина",
......@@ -4237,6 +4252,7 @@
"This feature is experimental. Configuration format and behavior may change.": "Эта функция является экспериментальной. Формат конфигурации и поведение могут измениться.",
"This feature requires server-side WeChat configuration": "Эта функция требует серверной конфигурации WeChat",
"This identifier is sent to the payment backend when creating an order. Use alipay for Alipay, wxpay for WeChat Pay, stripe for Stripe. Custom values must be supported by your payment provider.": "Этот идентификатор отправляется в платежный backend при создании заказа. Для Alipay используйте alipay, для WeChat Pay — wxpay, для Stripe — stripe. Пользовательские значения должны поддерживаться вашим платежным провайдером.",
"This instance is using an automatic hostname. Set NODE_NAME to a stable unique value for multi-instance management.": "Этот экземпляр использует автоматическое имя хоста. Задайте стабильное уникальное значение NODE_NAME для управления несколькими экземплярами.",
"This may cause cache failures.": "Это может привести к сбоям кэша.",
"This may take a few moments while we validate the request and update your session.": "Это может занять несколько мгновений, пока мы проверяем запрос и обновляем вашу сессию.",
"This model has both fixed price and ratio billing conflicts": "Эта модель имеет конфликты как фиксированной цены, так и пропорциональной тарификации",
......@@ -4561,6 +4577,7 @@
"USD price per 1M tokens.": "Цена в USD за 1 млн токенов.",
"Use +: to add a group, -: to remove a default selectable group, or no prefix to append a group.": "Используйте +: для добавления группы, -: для удаления выбираемой по умолчанию группы, без префикса — для добавления в конец.",
"Use a compatible browser or device with biometric authentication or a security key to register a Passkey.": "Используйте совместимый браузер или устройство с биометрической аутентификацией или ключ безопасности для регистрации ключа доступа.",
"Use a different stable value for each instance, then restart the service.": "Используйте разные стабильные значения для каждого экземпляра, затем перезапустите сервис.",
"Use a path to append it to the channel Base URL, or enter a full URL to override the Base URL for this route.": "Укажите путь, чтобы добавить его к Base URL канала, или введите полный URL, чтобы переопределить Base URL для этого маршрута.",
"Use authenticator code": "Использовать код аутентификатора",
"Use backup code": "Использовать резервный код",
......@@ -4663,6 +4680,7 @@
"Verify Setup": "Проверить настройку",
"Verify your database connection": "Проверьте подключение к базе данных",
"Verifying credentials and pulling stores from your Pancake account...": "Проверяем учетные данные и загружаем магазины из вашего аккаунта Pancake...",
"Version": "Версия",
"Version Overrides": "Переопределения версий",
"Vertex AI": "Vertex AI",
"Vertex AI API Key mode does not support batch creation": "Режим API Key Vertex AI не поддерживает пакетное создание",
......@@ -4734,8 +4752,9 @@
"Warning: Disabling 2FA will make your account less secure.": "Внимание: Отключение 2FA сделает вашу учетную запись менее безопасной.",
"Warning: This action is permanent and irreversible!": "Внимание: Это действие является постоянным и необратимым!",
"We apologize for the inconvenience.": "Приносим извинения за неудобства.",
"We could not load the setup status.": "Не удалось загрузить статус настройки.",
"We could not load instances.": "Не удалось загрузить экземпляры.",
"We could not load system tasks.": "Не удалось загрузить системные задачи.",
"We could not load the setup status.": "Не удалось загрузить статус настройки.",
"We will prompt your device to confirm using biometrics or your hardware key.": "Мы предложим вашему устройству подтвердить действие с помощью биометрии или аппаратного ключа.",
"We'll be back online shortly.": "Мы скоро вернемся в сеть.",
"Web search": "Веб-поиск",
......@@ -4798,6 +4817,7 @@
"with the API key from your token settings.": "на API-ключ из настроек токенов.",
"Without additional conditions, only the type above is used for pruning.": "Без дополнительных условий для очистки используется только тип выше.",
"Worker Access Key": "Ключ доступа воркера",
"Worker instances do not run master-only background tasks.": "Экземпляры worker не выполняют фоновые задачи только для master.",
"Worker Proxy": "Прокси воркера",
"Worker URL": "URL воркера",
"Workspaces": "Рабочие пространства",
......
......@@ -915,6 +915,7 @@
"Configure keyword filtering for prompts and responses.": "Định cấu hình lọc từ khóa để xem lời nhắc và câu trả lời.",
"Configure model, caching, and group ratios used for billing": "Cấu hình mô hình, bộ nhớ đệm và tỷ lệ nhóm được sử dụng để tính phí.",
"Configure monitoring status page groups for the dashboard": "Cấu hình các nhóm trang trạng thái giám sát cho bảng điều khiển",
"Configure NODE_NAME": "Cấu hình NODE_NAME",
"Configure per-model ratio for image inputs or outputs.": "Cấu hình tỷ lệ theo mô hình cho đầu vào hoặc đầu ra hình ảnh.",
"Configure per-tool unit prices ($/1K calls). Per-request models do not incur additional tool fees.": "Cấu hình giá theo từng công cụ ($/1K lần gọi). Mô hình tính phí theo request không phát sinh thêm phí công cụ.",
"Configure pricing ratios for a specific model.": "Cấu hình tỷ lệ định giá cho một mô hình cụ thể.",
......@@ -1052,6 +1053,7 @@
"Cost Tracking": "Theo dõi chi phí",
"Count must be between {{min}} and {{max}}": "Số lượng phải nằm trong khoảng từ {{min}} đến {{max}}.",
"Coze": "Coze",
"CPU": "CPU",
"CPU Threshold (%)": "Ngưỡng CPU (%)",
"Create": "Tạo",
"Create a copy of:": "Tạo bản sao của:",
......@@ -1935,6 +1937,7 @@
"Format: AppId|SecretId|SecretKey": "Định dạng: AppId|SecretId|SecretKey",
"Forward requests directly to upstream providers without any post-processing.": "Chuyển tiếp các yêu cầu trực tiếp đến các nhà cung cấp ngược dòng mà không cần xử lý hậu kỳ nào.",
"Frames per second": "Khung hình / giây",
"Free": "Trống",
"Free: {{free}} / Total: {{total}}": "Còn trống: {{free}} / Tổng: {{total}}",
"Friendly name to identify this channel": "Tên thân thiện để nhận dạng kênh này",
"From Address": "Địa chỉ Người gửi",
......@@ -2186,6 +2189,7 @@
"Inspect requests, errors, and billing details": "Kiểm tra yêu cầu, lỗi và chi tiết thanh toán",
"Inspect user prompts": "Kiểm tra lời nhắc của người dùng",
"Instance": "Phiên bản",
"Instances": "Phiên bản",
"Insufficient balance": "Số dư không đủ",
"Integrations": "Tích hợp",
"Inter-group overrides": "Ghi đè liên nhóm",
......@@ -2287,7 +2291,7 @@
"Last check time": "Thời gian kiểm tra gần nhất",
"Last detected addable models": "Mô hình có thể thêm được phát hiện gần nhất",
"Last Login": "Lần đăng nhập cuối",
"Last Seen": "Lần cuối",
"Last Seen": "Lần cuối thấy",
"Last Tested": "Được kiểm tra lần cuối",
"Last updated:": "Cập nhật lần cuối:",
"Last Used": "Dùng lần cuối",
......@@ -2411,6 +2415,7 @@
"Map upstream status codes to different codes": "Ánh xạ mã trạng thái upstream sang các mã khác",
"Market Share": "Thị phần",
"Marketing": "Tiếp thị",
"Master instances run scheduled background tasks.": "Phiên bản master chạy các tác vụ nền theo lịch.",
"Match All (AND)": "Tất cả khớp (AND)",
"Match Any (OR)": "Bất kỳ khớp (OR)",
"Match Mode": "Chế độ khớp",
......@@ -2449,6 +2454,7 @@
"Media pricing": "Giá phương tiện",
"Median time-to-first-token (TTFT) sampled hourly per group": "Độ trễ token đầu tiên trung vị (TTFT) lấy mẫu mỗi giờ theo nhóm",
"Medical Q&A, mental health support": "Hỏi đáp y tế, hỗ trợ sức khỏe tinh thần",
"Memory": "Bộ nhớ",
"Memory Hits": "Lượt truy cập bộ nhớ",
"Memory Threshold (%)": "Ngưỡng bộ nhớ (%)",
"Merchant ID": "Mã thương gia",
......@@ -2710,6 +2716,7 @@
"No discount tiers configured. Click \"Add discount tier\" to get started.": "Chưa cấu hình cấp chiết khấu nào. Nhấp vào \"Thêm cấp chiết khấu\" để bắt đầu.",
"No duplicate keys found": "Không tìm thấy khóa trùng lặp",
"No enabled tokens available": "Không có token nào được kích hoạt",
"No encryption": "Không mã hóa",
"No endpoints configured. Switch to JSON mode or add rows to define endpoints.": "Chưa cấu hình endpoint nào. Chuyển sang chế độ JSON hoặc thêm hàng để định nghĩa endpoint.",
"No FAQ entries available": "Không có mục FAQ nào.",
"No FAQ entries yet. Click \"Add FAQ\" to create one.": "Chưa có mục FAQ nào. Nhấp vào \"Thêm FAQ\" để tạo một mục.",
......@@ -2724,6 +2731,7 @@
"No history data available": "Không có dữ liệu lịch sử",
"No incidents in the last 24 hours": "Không có sự cố trong 24 giờ qua",
"No incidents in the last 30 days": "Không có sự cố trong 30 ngày qua",
"No instances have reported yet.": "Chưa có phiên bản nào báo cáo.",
"No Inviter": "Không có người mời",
"No keys found": "Không tìm thấy khóa",
"No latency data available": "Không có dữ liệu độ trễ",
......@@ -2820,10 +2828,11 @@
"Node": "Nút",
"Node filters": "Bộ lọc nút",
"Node Name": "Tên nút",
"Node role": "Vai trò node",
"Nodes reporting from this deployment and their latest heartbeat.": "Các node đang báo cáo từ bản triển khai này và nhịp tim mới nhất của chúng.",
"Non-stream": "Không phát trực tuyến",
"Non-zero invitation rewards require compliance confirmation in Payment Gateway settings.": "Phần thưởng mời khác 0 yêu cầu xác nhận tuân thủ trong cài đặt Cổng thanh toán.",
"None": "Không có",
"No encryption": "Không mã hóa",
"noreply@example.com": "noreply@example.com",
"Normalized:": "Chuẩn hóa:",
"Not available": "Không khả dụng",
......@@ -2890,6 +2899,7 @@
"One IP or CIDR range per line": "Một IP hoặc dải CIDR mỗi dòng",
"One IP per line (empty for no restriction)": "Mỗi IP một dòng (để trống nếu không giới hạn)",
"one keyword per line": "Mỗi dòng một từ khóa",
"online": "trực tuyến",
"Online": "Trực tuyến",
"Online payment is not enabled. Please contact the administrator.": "Thanh toán trực tuyến chưa được kích hoạt. Vui lòng liên hệ quản trị viên.",
"Online topup is not enabled. Please use redemption code or contact administrator.": "Tính năng nạp tiền trực tuyến chưa được bật. Vui lòng sử dụng mã quy đổi hoặc liên hệ quản trị viên.",
......@@ -3599,6 +3609,7 @@
"Resetting...": "Đang đặt lại...",
"Resolve Conflicts": "Giải quyết Xung đột",
"Resource Configuration": "Cấu hình tài nguyên",
"Resources": "Tài nguyên",
"Response": "Phản hồi",
"Response Time": "Thời gian phản hồi",
"Response time: {{duration}}": "Thời gian phản hồi: {{duration}}",
......@@ -3668,6 +3679,7 @@
"Run tests for the selected models": "Chạy kiểm thử cho các mô hình đã chọn",
"running": "đang chạy",
"Running": "Đang chạy",
"Runtime": "Môi trường chạy",
"Runway": "Thời gian còn lại",
"s": "s",
"Safety Settings": "Cài đặt an toàn",
......@@ -3946,7 +3958,6 @@
"Slug is required": "Slug là bắt buộc",
"Slug must be less than 100 characters": "Slug phải ít hơn 100 ký tự",
"Smallest USD amount users can recharge (Epay)": "Số tiền USD tối thiểu người dùng có thể nạp (Epay)",
"SSL/TLS": "SSL/TLS",
"SMTP Email": "Email SMTP",
"SMTP encryption": "Mã hóa SMTP",
"SMTP Host": "Máy chủ SMTP",
......@@ -3975,15 +3986,18 @@
"Special usable group rules can add, remove, or append selectable token groups for a specific user group.": "Quy tắc nhóm khả dụng đặc biệt có thể thêm, xóa hoặc nối nhóm token có thể chọn cho một nhóm người dùng cụ thể.",
"Spend limited": "Đã giới hạn chi tiêu",
"SQLite stores all data in a single file. Make sure that file is persisted when running in containers.": "SQLite lưu trữ tất cả dữ liệu trong một tệp duy nhất. Đảm bảo tệp được lưu trữ lâu dài khi chạy trong container.",
"SSL/TLS": "SSL/TLS",
"SSRF Protection": "Bảo vệ SSRF",
"stale": "mất kết nối",
"Standard": "Tiêu chuẩn",
"Standard price": "Giá tiêu chuẩn",
"STARTTLS": "STARTTLS",
"Start": "Bắt đầu",
"Start a conversation to see messages here": "Bắt đầu một cuộc trò chuyện để xem tin nhắn tại đây",
"Start collecting payments globally without registering a company. Built for indie developers, OPC sole proprietorships, and startups. Waffo Pancake acts as your Merchant of Record, taking on the compliance burden of global payment collection — consumption tax, invoicing, subscription management, refunds, and chargebacks. Solo developers can launch fast and stay focused on product instead of compliance. Onboard in minutes — one prompt to a full integration.": "Bắt đầu thu thanh toán toàn cầu mà không cần đăng ký công ty. Dành cho lập trình viên độc lập, chủ sở hữu OPC và startup. Waffo Pancake đóng vai trò Merchant of Record, chịu trách nhiệm tuân thủ cho việc thu thanh toán toàn cầu — thuế tiêu dùng, hóa đơn, quản lý đăng ký, hoàn tiền và tranh chấp thanh toán. Lập trình viên cá nhân có thể ra mắt nhanh và tập trung vào sản phẩm thay vì tuân thủ. Onboard trong vài phút — từ một prompt đến tích hợp hoàn chỉnh.",
"Start for free with generous limits. No credit card required.": "Bắt đầu miễn phí với giới hạn hào phóng. Không cần thẻ tín dụng.",
"Start Time": "Thời gian bắt đầu",
"Started": "Đã khởi động",
"STARTTLS": "STARTTLS",
"Static page describing the platform.": "Trang tĩnh mô tả nền tảng.",
"Statistical count": "Số đếm thống kê",
"Statistical quota": "Chỉ tiêu thống kê",
......@@ -4006,6 +4020,7 @@
"Stop testing": "Dừng kiểm thử",
"Stopping batch test...": "Đang dừng kiểm thử hàng loạt...",
"Stopping...": "Đang dừng...",
"Storage": "Lưu trữ",
"Store": "Store",
"Store + product created": "Đã tạo cửa hàng + sản phẩm",
"Store ID": "Mã cửa hàng",
......@@ -4237,6 +4252,7 @@
"This feature is experimental. Configuration format and behavior may change.": "Tính năng này đang ở giai đoạn thử nghiệm. Định dạng cấu hình và hành vi có thể thay đổi.",
"This feature requires server-side WeChat configuration": "Tính năng này yêu cầu cấu hình WeChat phía máy chủ",
"This identifier is sent to the payment backend when creating an order. Use alipay for Alipay, wxpay for WeChat Pay, stripe for Stripe. Custom values must be supported by your payment provider.": "Mã định danh này được gửi tới backend thanh toán khi tạo đơn hàng. Dùng alipay cho Alipay, wxpay cho WeChat Pay, stripe cho Stripe. Giá trị tùy chỉnh phải được nhà cung cấp thanh toán hỗ trợ.",
"This instance is using an automatic hostname. Set NODE_NAME to a stable unique value for multi-instance management.": "Phiên bản này đang dùng hostname tự động. Hãy đặt NODE_NAME thành một giá trị ổn định và duy nhất để quản lý nhiều phiên bản.",
"This may cause cache failures.": "Điều này có thể gây ra lỗi bộ nhớ đệm.",
"This may take a few moments while we validate the request and update your session.": "Việc này có thể mất vài phút trong khi chúng tôi xác thực yêu cầu và cập nhật phiên của bạn.",
"This model has both fixed price and ratio billing conflicts": "Mô hình này có cả mâu thuẫn về thanh toán theo giá cố định và theo tỷ lệ.",
......@@ -4561,6 +4577,7 @@
"USD price per 1M tokens.": "Giá USD cho mỗi 1 triệu token.",
"Use +: to add a group, -: to remove a default selectable group, or no prefix to append a group.": "Dùng +: để thêm nhóm, -: để xóa nhóm có thể chọn mặc định, hoặc không có tiền tố để nối nhóm.",
"Use a compatible browser or device with biometric authentication or a security key to register a Passkey.": "Sử dụng trình duyệt hoặc thiết bị tương thích có xác thực sinh trắc học hoặc khóa bảo mật để đăng ký Khóa truy cập.",
"Use a different stable value for each instance, then restart the service.": "Dùng một giá trị ổn định khác nhau cho mỗi phiên bản, sau đó khởi động lại dịch vụ.",
"Use a path to append it to the channel Base URL, or enter a full URL to override the Base URL for this route.": "Dùng đường dẫn để nối vào Base URL của kênh, hoặc nhập URL đầy đủ để ghi đè Base URL cho tuyến này.",
"Use authenticator code": "Sử dụng mã xác thực",
"Use backup code": "Sử dụng mã dự phòng",
......@@ -4663,6 +4680,7 @@
"Verify Setup": "Xác minh thiết lập",
"Verify your database connection": "Xác minh kết nối cơ sở dữ liệu của bạn",
"Verifying credentials and pulling stores from your Pancake account...": "Đang xác minh thông tin xác thực và lấy cửa hàng từ tài khoản Pancake của bạn...",
"Version": "Phiên bản",
"Version Overrides": "Ghi đè phiên bản",
"Vertex AI": "Vertex AI",
"Vertex AI API Key mode does not support batch creation": "Chế độ API Key của Vertex AI không hỗ trợ tạo hàng loạt",
......@@ -4734,8 +4752,9 @@
"Warning: Disabling 2FA will make your account less secure.": "Cảnh báo: Vô hiệu hóa 2FA sẽ khiến tài khoản của bạn kém an toàn hơn.",
"Warning: This action is permanent and irreversible!": "Cảnh báo: Hành động này là vĩnh viễn và không thể đảo ngược!",
"We apologize for the inconvenience.": "Chúng tôi xin lỗi vì sự bất tiện này.",
"We could not load the setup status.": "Chúng tôi không thể tải trạng thái thiết lập.",
"We could not load instances.": "Không thể tải danh sách phiên bản.",
"We could not load system tasks.": "Không thể tải tác vụ hệ thống.",
"We could not load the setup status.": "Chúng tôi không thể tải trạng thái thiết lập.",
"We will prompt your device to confirm using biometrics or your hardware key.": "Chúng tôi sẽ yêu cầu thiết bị của bạn xác nhận bằng cách sử dụng sinh trắc học hoặc khóa bảo mật phần cứng của bạn.",
"We'll be back online shortly.": "Chúng tôi sẽ sớm trực tuyến trở lại.",
"Web search": "Tìm kiếm web",
......@@ -4798,6 +4817,7 @@
"with the API key from your token settings.": "bằng API key từ trang Tokens của bạn.",
"Without additional conditions, only the type above is used for pruning.": "Không có điều kiện bổ sung, chỉ type ở trên được sử dụng để dọn dẹp.",
"Worker Access Key": "Khóa truy cập nhân viên",
"Worker instances do not run master-only background tasks.": "Phiên bản worker không chạy các tác vụ nền chỉ dành cho master.",
"Worker Proxy": "Proxy Nhân viên",
"Worker URL": "URL của Worker",
"Workspaces": "Không gian làm việc",
......
......@@ -915,6 +915,7 @@
"Configure keyword filtering for prompts and responses.": "配置用于提示和响应的关键词过滤。",
"Configure model, caching, and group ratios used for billing": "配置用于计费的模型、缓存和分组比例",
"Configure monitoring status page groups for the dashboard": "配置用于仪表板的监控状态页面分组",
"Configure NODE_NAME": "配置 NODE_NAME",
"Configure per-model ratio for image inputs or outputs.": "配置图像输入或输出的每模型比例。",
"Configure per-tool unit prices ($/1K calls). Per-request models do not incur additional tool fees.": "为每个工具配置单价($/1K 次调用)。按请求计费的模型不额外收取工具费用。",
"Configure pricing ratios for a specific model.": "配置特定模型的定价比例。",
......@@ -1052,6 +1053,7 @@
"Cost Tracking": "成本跟踪",
"Count must be between {{min}} and {{max}}": "计数必须介于{{min}}和{{max}}之间",
"Coze": "Coze",
"CPU": "CPU",
"CPU Threshold (%)": "CPU 阈值 (%)",
"Create": "新建",
"Create a copy of:": "创建副本:",
......@@ -1935,6 +1937,7 @@
"Format: AppId|SecretId|SecretKey": "格式:AppId|SecretId|SecretKey",
"Forward requests directly to upstream providers without any post-processing.": "将请求直接转发给上游提供商,不进行任何后处理。",
"Frames per second": "帧率",
"Free": "可用",
"Free: {{free}} / Total: {{total}}": "可用空间: {{free}} / 总空间: {{total}}",
"Friendly name to identify this channel": "用于识别此渠道的友好名称",
"From Address": "发件地址",
......@@ -2186,6 +2189,7 @@
"Inspect requests, errors, and billing details": "查看请求、错误和计费详情",
"Inspect user prompts": "检查用户提示",
"Instance": "实例",
"Instances": "实例",
"Insufficient balance": "余额不足",
"Integrations": "集成",
"Inter-group overrides": "分组间覆盖",
......@@ -2287,7 +2291,7 @@
"Last check time": "上次检测时间",
"Last detected addable models": "上次检测到可加入模型",
"Last Login": "最后登录",
"Last Seen": "最近一次",
"Last Seen": "最后上报",
"Last Tested": "上次测试",
"Last updated:": "上次更新时间:",
"Last Used": "最后使用时间",
......@@ -2411,6 +2415,7 @@
"Map upstream status codes to different codes": "将上游状态码映射到不同的代码",
"Market Share": "市场份额",
"Marketing": "市场营销",
"Master instances run scheduled background tasks.": "master 实例执行定时后台任务。",
"Match All (AND)": "必须全部满足(AND)",
"Match Any (OR)": "满足任一条件(OR)",
"Match Mode": "匹配方式",
......@@ -2449,6 +2454,7 @@
"Media pricing": "媒体定价",
"Median time-to-first-token (TTFT) sampled hourly per group": "按小时采样的各分组首 token 延迟(TTFT)中位数",
"Medical Q&A, mental health support": "医疗问答与心理健康支持",
"Memory": "内存",
"Memory Hits": "内存命中",
"Memory Threshold (%)": "内存阈值 (%)",
"Merchant ID": "商户 ID",
......@@ -2710,6 +2716,7 @@
"No discount tiers configured. Click \"Add discount tier\" to get started.": "未配置折扣等级。点击“添加折扣等级”即可开始使用。",
"No duplicate keys found": "未发现重复密钥",
"No enabled tokens available": "当前没有可用的启用令牌",
"No encryption": "无加密",
"No endpoints configured. Switch to JSON mode or add rows to define endpoints.": "未配置端点。切换到 JSON 模式或添加行来定义端点。",
"No FAQ entries available": "暂无 FAQ 条目",
"No FAQ entries yet. Click \"Add FAQ\" to create one.": "暂无常见问题条目。点击“添加常见问题”来创建一个。",
......@@ -2724,6 +2731,7 @@
"No history data available": "暂无历史数据",
"No incidents in the last 24 hours": "最近 24 小时无异常",
"No incidents in the last 30 days": "最近 30 天无事件",
"No instances have reported yet.": "暂无实例上报。",
"No Inviter": "无邀请人",
"No keys found": "未找到密钥",
"No latency data available": "暂无延迟数据",
......@@ -2820,10 +2828,11 @@
"Node": "节点",
"Node filters": "节点筛选",
"Node Name": "节点名称",
"Node role": "节点职责",
"Nodes reporting from this deployment and their latest heartbeat.": "当前部署中上报的节点及其最新心跳。",
"Non-stream": "非流式",
"Non-zero invitation rewards require compliance confirmation in Payment Gateway settings.": "非零邀请奖励需要先在支付网关设置中确认合规条款。",
"None": "无",
"No encryption": "无加密",
"noreply@example.com": "noreply@example.com",
"Normalized:": "已归一化:",
"Not available": "不可用",
......@@ -2890,6 +2899,7 @@
"One IP or CIDR range per line": "每行一个 IP 或 CIDR 范围",
"One IP per line (empty for no restriction)": "每行一个 IP (留空表示无限制)",
"one keyword per line": "每行一个关键词",
"online": "在线",
"Online": "在线",
"Online payment is not enabled. Please contact the administrator.": "管理员未开启在线支付功能,请联系管理员配置。",
"Online topup is not enabled. Please use redemption code or contact administrator.": "尚未启用在线充值。请使用兑换码或联系管理员。",
......@@ -3599,6 +3609,7 @@
"Resetting...": "重置中...",
"Resolve Conflicts": "解决冲突",
"Resource Configuration": "资源配置",
"Resources": "资源",
"Response": "响应",
"Response Time": "响应时间",
"Response time: {{duration}}": "响应时间:{{duration}}",
......@@ -3668,6 +3679,7 @@
"Run tests for the selected models": "运行所选模型的测试",
"running": "运行中",
"Running": "运行中",
"Runtime": "运行环境",
"Runway": "可用时长",
"s": "秒",
"Safety Settings": "安全设置",
......@@ -3946,7 +3958,6 @@
"Slug is required": "Slug 不能为空",
"Slug must be less than 100 characters": "Slug 不能超过 100 个字符",
"Smallest USD amount users can recharge (Epay)": "用户可以充值的最小美元金额 (Epay)",
"SSL/TLS": "SSL/TLS",
"SMTP Email": "SMTP 邮箱",
"SMTP encryption": "SMTP 加密方式",
"SMTP Host": "SMTP 主机",
......@@ -3975,15 +3986,18 @@
"Special usable group rules can add, remove, or append selectable token groups for a specific user group.": "特殊可用分组规则可以为特定用户分组添加、移除或追加可选令牌分组。",
"Spend limited": "消费受限",
"SQLite stores all data in a single file. Make sure that file is persisted when running in containers.": "SQLite 将所有数据存储在单个文件中。在容器中运行时请确保该文件已持久化。",
"SSL/TLS": "SSL/TLS",
"SSRF Protection": "SSRF 保护",
"stale": "失联",
"Standard": "标准",
"Standard price": "标准价格",
"STARTTLS": "STARTTLS",
"Start": "开始",
"Start a conversation to see messages here": "开始对话以在此处查看消息",
"Start collecting payments globally without registering a company. Built for indie developers, OPC sole proprietorships, and startups. Waffo Pancake acts as your Merchant of Record, taking on the compliance burden of global payment collection — consumption tax, invoicing, subscription management, refunds, and chargebacks. Solo developers can launch fast and stay focused on product instead of compliance. Onboard in minutes — one prompt to a full integration.": "无需注册公司即可开始全球收款。面向独立开发者、OPC 个体经营者和初创团队构建。Waffo Pancake 作为你的登记商户(Merchant of Record),承担全球收款相关的合规负担,包括消费税、开票、订阅管理、退款和拒付。个人开发者可以快速上线,专注产品而不是合规事务。几分钟即可完成入驻,从一个提示词到完整集成。",
"Start for free with generous limits. No credit card required.": "免费开始使用,额度充足,无需绑定信用卡。",
"Start Time": "起始时间",
"Started": "启动时间",
"STARTTLS": "STARTTLS",
"Static page describing the platform.": "描述平台的静态页面。",
"Statistical count": "统计计数",
"Statistical quota": "统计配额",
......@@ -4006,6 +4020,7 @@
"Stop testing": "停止测试",
"Stopping batch test...": "正在停止批量测试...",
"Stopping...": "正在停止...",
"Storage": "存储",
"Store": "店铺",
"Store + product created": "店铺和产品已创建",
"Store ID": "商店 ID",
......@@ -4237,6 +4252,7 @@
"This feature is experimental. Configuration format and behavior may change.": "此功能为实验性功能。配置格式和行为可能会发生变化。",
"This feature requires server-side WeChat configuration": "此功能需要服务器端微信配置",
"This identifier is sent to the payment backend when creating an order. Use alipay for Alipay, wxpay for WeChat Pay, stripe for Stripe. Custom values must be supported by your payment provider.": "创建订单时会把这个标识提交给支付后端。支付宝填 alipay,微信填 wxpay,Stripe 填 stripe。自定义值必须是支付服务支持的标识。",
"This instance is using an automatic hostname. Set NODE_NAME to a stable unique value for multi-instance management.": "该实例正在使用自动主机名。请设置稳定且唯一的 NODE_NAME,以便进行多实例管理。",
"This may cause cache failures.": "这可能导致缓存故障。",
"This may take a few moments while we validate the request and update your session.": "这可能需要一些时间,因为我们正在验证请求并更新您的会话。",
"This model has both fixed price and ratio billing conflicts": "此模型同时存在固定价格和比例计费冲突",
......@@ -4561,6 +4577,7 @@
"USD price per 1M tokens.": "每 100 万 token 的美元价格。",
"Use +: to add a group, -: to remove a default selectable group, or no prefix to append a group.": "使用 +: 添加分组,使用 -: 移除默认可选分组,不加前缀则追加分组。",
"Use a compatible browser or device with biometric authentication or a security key to register a Passkey.": "请使用支持生物识别认证或安全密钥的兼容浏览器或设备来注册通行密钥。",
"Use a different stable value for each instance, then restart the service.": "每个实例使用不同且稳定的值,然后重启服务。",
"Use a path to append it to the channel Base URL, or enter a full URL to override the Base URL for this route.": "填写以 / 开头的路径时会自动拼接渠道 Base URL;填写完整 URL 时,此路由会直接使用该 URL。",
"Use authenticator code": "使用验证器代码",
"Use backup code": "使用备用代码",
......@@ -4663,6 +4680,7 @@
"Verify Setup": "验证设置",
"Verify your database connection": "验证数据库连接",
"Verifying credentials and pulling stores from your Pancake account...": "正在验证凭证并从你的 Pancake 账户拉取店铺...",
"Version": "版本",
"Version Overrides": "版本覆盖",
"Vertex AI": "Vertex AI",
"Vertex AI API Key mode does not support batch creation": "Vertex AI API Key 模式不支持批量创建",
......@@ -4734,8 +4752,9 @@
"Warning: Disabling 2FA will make your account less secure.": "警告:禁用双重身份验证将使您的账户安全性降低。",
"Warning: This action is permanent and irreversible!": "警告:此操作是永久且不可逆的!",
"We apologize for the inconvenience.": "对于由此造成的不便,我们深表歉意。",
"We could not load the setup status.": "我们无法加载设置状态。",
"We could not load instances.": "无法加载实例信息。",
"We could not load system tasks.": "无法加载系统任务。",
"We could not load the setup status.": "我们无法加载设置状态。",
"We will prompt your device to confirm using biometrics or your hardware key.": "我们将提示您的设备使用生物识别或硬件密钥进行确认。",
"We'll be back online shortly.": "我们将很快恢复在线。",
"Web search": "网络搜索",
......@@ -4798,6 +4817,7 @@
"with the API key from your token settings.": "替换为令牌设置中的 API Key。",
"Without additional conditions, only the type above is used for pruning.": "未添加附加条件时,仅使用上方 type 进行清理。",
"Worker Access Key": "Worker 访问密钥",
"Worker instances do not run master-only background tasks.": "worker 实例不执行仅限 master 的后台任务。",
"Worker Proxy": "Worker 代理",
"Worker URL": "Worker URL",
"Workspaces": "工作区",
......
......@@ -45,6 +45,12 @@ export const STATIC_I18N_KEYS = [
'Routing Reliability',
'Maintenance',
// System info
'online',
'stale',
'Master instances run scheduled background tasks.',
'Worker instances do not run master-only background tasks.',
// Pricing constants
'Name',
'Price: Low to High',
......
......@@ -145,6 +145,46 @@ export function formatTimestampToDate(
return dayjs(ms).format('YYYY-MM-DD HH:mm:ss')
}
/**
* Format timestamp as relative time, e.g. "30 seconds ago".
* @param timestamp - Timestamp in seconds or milliseconds
* @param unit - Unit of the timestamp ('seconds' or 'milliseconds')
* @param locales - Locale passed to Intl.RelativeTimeFormat
*/
export function formatTimestampRelative(
timestamp?: number,
unit: 'seconds' | 'milliseconds' = 'seconds',
locales?: Intl.LocalesArgument
): string {
if (!timestamp || timestamp === -1 || timestamp === 0) {
return '-'
}
const ms = unit === 'seconds' ? timestamp * 1000 : timestamp
const diffSeconds = Math.round((ms - Date.now()) / 1000)
const absSeconds = Math.abs(diffSeconds)
const formatter = new Intl.RelativeTimeFormat(locales, {
numeric: 'always',
})
if (absSeconds < 60) {
return formatter.format(diffSeconds, 'second')
}
if (absSeconds < 3600) {
return formatter.format(Math.round(diffSeconds / 60), 'minute')
}
if (absSeconds < 86400) {
return formatter.format(Math.round(diffSeconds / 3600), 'hour')
}
if (absSeconds < 2592000) {
return formatter.format(Math.round(diffSeconds / 86400), 'day')
}
if (absSeconds < 31536000) {
return formatter.format(Math.round(diffSeconds / 2592000), 'month')
}
return formatter.format(Math.round(diffSeconds / 31536000), 'year')
}
/** Format a Date object to YYYY-MM-DD HH:mm:ss */
export function formatDateTimeStr(date: Date): string {
return dayjs(date).format('YYYY-MM-DD HH:mm:ss')
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment