Commit efd6c445 by Herb Brewer Committed by GitHub

feat: add passive channel monitoring mode (#5592)

* feat: add passive channel monitoring mode

* fix: clarify passive monitor mode copy
parent e5694748
......@@ -893,12 +893,7 @@ func TestChannel(c *gin.Context) {
var testAllChannelsLock sync.Mutex
var testAllChannelsRunning bool = false
func testAllChannels(notify bool) error {
testUserID, err := resolveChannelTestUserID(nil)
if err != nil {
return err
}
func testChannels(channels []*model.Channel, testUserID int, notify bool, allowDisable bool) error {
testAllChannelsLock.Lock()
if testAllChannelsRunning {
testAllChannelsLock.Unlock()
......@@ -906,10 +901,6 @@ func testAllChannels(notify bool) error {
}
testAllChannelsRunning = true
testAllChannelsLock.Unlock()
channels, getChannelErr := model.GetAllChannels(0, 0, true, false)
if getChannelErr != nil {
return getChannelErr
}
var disableThreshold = int64(common.ChannelDisableThreshold * 1000)
if disableThreshold == 0 {
disableThreshold = 10000000 // a impossible value
......@@ -949,12 +940,12 @@ func testAllChannels(notify bool) error {
}
// disable channel
if isChannelEnabled && shouldBanChannel && channel.GetAutoBan() {
if allowDisable && isChannelEnabled && shouldBanChannel && channel.GetAutoBan() {
processChannelError(result.context, *types.NewChannelError(channel.Id, channel.Type, channel.Name, channel.ChannelInfo.IsMultiKey, common.GetContextKeyString(result.context, constant.ContextKeyChannelKey), channel.GetAutoBan()), newAPIError)
}
// enable channel
if !isChannelEnabled && service.ShouldEnableChannel(newAPIError, channel.Status) {
if result.localErr == nil && !isChannelEnabled && service.ShouldEnableChannel(newAPIError, channel.Status) {
service.EnableChannel(channel.Id, common.GetContextKeyString(result.context, constant.ContextKeyChannelKey), channel.Name)
}
......@@ -969,6 +960,44 @@ func testAllChannels(notify bool) error {
return nil
}
func selectChannelsForAutomaticTest(channels []*model.Channel, mode string) []*model.Channel {
selected := make([]*model.Channel, 0, len(channels))
for _, channel := range channels {
if channel.Status == common.ChannelStatusManuallyDisabled {
continue
}
if mode == operation_setting.ChannelTestModePassiveRecovery && channel.Status != common.ChannelStatusAutoDisabled {
continue
}
selected = append(selected, channel)
}
return selected
}
func testAllChannels(notify bool) error {
testUserID, err := resolveChannelTestUserID(nil)
if err != nil {
return err
}
channels, getChannelErr := model.GetAllChannels(0, 0, true, false)
if getChannelErr != nil {
return getChannelErr
}
return testChannels(selectChannelsForAutomaticTest(channels, operation_setting.ChannelTestModeScheduledAll), testUserID, notify, true)
}
func testAutoDisabledChannels(notify bool) error {
testUserID, err := resolveChannelTestUserID(nil)
if err != nil {
return err
}
channels, getChannelErr := model.GetAllChannels(0, 0, true, false)
if getChannelErr != nil {
return getChannelErr
}
return testChannels(selectChannelsForAutomaticTest(channels, operation_setting.ChannelTestModePassiveRecovery), testUserID, notify, false)
}
func TestAllChannels(c *gin.Context) {
err := testAllChannels(true)
if err != nil {
......@@ -998,8 +1027,13 @@ func AutomaticallyTestChannels() {
frequency := operation_setting.GetMonitorSetting().AutoTestChannelMinutes
time.Sleep(time.Duration(int(math.Round(frequency))) * time.Minute)
common.SysLog(fmt.Sprintf("automatically test channels with interval %f minutes", frequency))
common.SysLog("automatically testing all channels")
_ = testAllChannels(false)
if operation_setting.GetMonitorSetting().ChannelTestMode == operation_setting.ChannelTestModePassiveRecovery {
common.SysLog("automatically testing auto-disabled channels")
_ = testAutoDisabledChannels(false)
} else {
common.SysLog("automatically testing all channels")
_ = testAllChannels(false)
}
common.SysLog("automatically channel test finished")
if !operation_setting.GetMonitorSetting().AutoTestChannelEnabled {
break
......
......@@ -6,8 +6,10 @@ import (
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/dto"
"github.com/QuantumNous/new-api/model"
"github.com/QuantumNous/new-api/pkg/billingexpr"
relaycommon "github.com/QuantumNous/new-api/relay/common"
"github.com/QuantumNous/new-api/setting/operation_setting"
"github.com/QuantumNous/new-api/types"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
......@@ -80,3 +82,30 @@ func TestResolveChannelTestUserIDUsesRequestUser(t *testing.T) {
require.NoError(t, err)
require.Equal(t, 2, userID)
}
func TestSelectChannelsForAutomaticTestPassiveRecoveryOnlyUsesAutoDisabled(t *testing.T) {
channels := []*model.Channel{
{Id: 1, Status: common.ChannelStatusEnabled},
{Id: 2, Status: common.ChannelStatusAutoDisabled},
{Id: 3, Status: common.ChannelStatusManuallyDisabled},
}
selected := selectChannelsForAutomaticTest(channels, operation_setting.ChannelTestModePassiveRecovery)
require.Len(t, selected, 1)
require.Equal(t, 2, selected[0].Id)
}
func TestSelectChannelsForAutomaticTestScheduledSkipsManualDisabled(t *testing.T) {
channels := []*model.Channel{
{Id: 1, Status: common.ChannelStatusEnabled},
{Id: 2, Status: common.ChannelStatusAutoDisabled},
{Id: 3, Status: common.ChannelStatusManuallyDisabled},
}
selected := selectChannelsForAutomaticTest(channels, operation_setting.ChannelTestModeScheduledAll)
require.Len(t, selected, 2)
require.Equal(t, 1, selected[0].Id)
require.Equal(t, 2, selected[1].Id)
}
......@@ -10,12 +10,19 @@ import (
type MonitorSetting struct {
AutoTestChannelEnabled bool `json:"auto_test_channel_enabled"`
AutoTestChannelMinutes float64 `json:"auto_test_channel_minutes"`
ChannelTestMode string `json:"channel_test_mode"`
}
const (
ChannelTestModeScheduledAll = "scheduled_all"
ChannelTestModePassiveRecovery = "passive_recovery"
)
// 默认配置
var monitorSetting = MonitorSetting{
AutoTestChannelEnabled: false,
AutoTestChannelMinutes: 10,
ChannelTestMode: ChannelTestModeScheduledAll,
}
func init() {
......@@ -29,6 +36,7 @@ func GetMonitorSetting() *MonitorSetting {
if err == nil && frequency > 0 {
monitorSetting.AutoTestChannelEnabled = true
monitorSetting.AutoTestChannelMinutes = float64(frequency)
monitorSetting.ChannelTestMode = ChannelTestModeScheduledAll
}
}
if enabled, ok := os.LookupEnv("CHANNEL_TEST_ENABLED"); ok {
......@@ -37,5 +45,8 @@ func GetMonitorSetting() *MonitorSetting {
monitorSetting.AutoTestChannelEnabled = parsed
}
}
if monitorSetting.ChannelTestMode != ChannelTestModePassiveRecovery {
monitorSetting.ChannelTestMode = ChannelTestModeScheduledAll
}
return &monitorSetting
}
......@@ -210,6 +210,7 @@ export function ModelMutateDrawer({
'100-199,300-399,401-407,409-499,500-503,505-523,525-599',
'monitor_setting.auto_test_channel_enabled': false,
'monitor_setting.auto_test_channel_minutes': 10,
'monitor_setting.channel_test_mode': 'scheduled_all',
'channel_affinity_setting.enabled': false,
'channel_affinity_setting.switch_on_success': true,
'channel_affinity_setting.keep_on_channel_disabled': false,
......
......@@ -72,6 +72,7 @@ const defaultModelSettings: ModelSettings = {
'100-199,300-399,401-407,409-499,500-503,505-523,525-599',
'monitor_setting.auto_test_channel_enabled': false,
'monitor_setting.auto_test_channel_minutes': 10,
'monitor_setting.channel_test_mode': 'scheduled_all',
'channel_affinity_setting.enabled': false,
'channel_affinity_setting.switch_on_success': true,
'channel_affinity_setting.keep_on_channel_disabled': false,
......
......@@ -33,6 +33,14 @@ import {
FormMessage,
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Separator } from '@/components/ui/separator'
import { Switch } from '@/components/ui/switch'
import { Textarea } from '@/components/ui/textarea'
......@@ -53,6 +61,9 @@ const numericString = z.string().refine((value) => {
return !Number.isNaN(Number(trimmed)) && Number(trimmed) >= 0
}, 'Enter a non-negative number or leave empty')
const channelTestModes = ['scheduled_all', 'passive_recovery'] as const
type ChannelTestMode = (typeof channelTestModes)[number]
const routingReliabilitySchema = z
.object({
RetryTimes: z.coerce.number().min(0).max(10),
......@@ -68,6 +79,7 @@ const routingReliabilitySchema = z
.number()
.int()
.min(1, 'Interval must be at least 1 minute'),
channel_test_mode: z.enum(channelTestModes),
}),
})
.superRefine((values, ctx) => {
......@@ -112,6 +124,7 @@ type RoutingReliabilitySectionProps = {
AutomaticRetryStatusCodes: string
'monitor_setting.auto_test_channel_enabled': boolean
'monitor_setting.auto_test_channel_minutes': number
'monitor_setting.channel_test_mode': ChannelTestMode
}
}
......@@ -129,6 +142,11 @@ type NormalizedRoutingReliabilityValues = {
AutomaticRetryStatusCodes: string
'monitor_setting.auto_test_channel_enabled': boolean
'monitor_setting.auto_test_channel_minutes': number
'monitor_setting.channel_test_mode': ChannelTestMode
}
function normalizeChannelTestMode(value?: string): ChannelTestMode {
return value === 'passive_recovery' ? 'passive_recovery' : 'scheduled_all'
}
const buildFormDefaults = (
......@@ -148,6 +166,9 @@ const buildFormDefaults = (
defaults['monitor_setting.auto_test_channel_enabled'],
auto_test_channel_minutes:
defaults['monitor_setting.auto_test_channel_minutes'],
channel_test_mode: normalizeChannelTestMode(
defaults['monitor_setting.channel_test_mode']
),
},
})
......@@ -171,6 +192,9 @@ const normalizeDefaults = (
defaults['monitor_setting.auto_test_channel_enabled'],
'monitor_setting.auto_test_channel_minutes':
defaults['monitor_setting.auto_test_channel_minutes'],
'monitor_setting.channel_test_mode': normalizeChannelTestMode(
defaults['monitor_setting.channel_test_mode']
),
})
const normalizeFormValues = (
......@@ -193,6 +217,7 @@ const normalizeFormValues = (
values.monitor_setting.auto_test_channel_enabled,
'monitor_setting.auto_test_channel_minutes':
values.monitor_setting.auto_test_channel_minutes,
'monitor_setting.channel_test_mode': values.monitor_setting.channel_test_mode,
})
export function RoutingReliabilitySection({
......@@ -222,6 +247,7 @@ export function RoutingReliabilitySection({
const autoDisableStatusCodes = form.watch('AutomaticDisableStatusCodes')
const autoRetryStatusCodes = form.watch('AutomaticRetryStatusCodes')
const channelTestMode = form.watch('monitor_setting.channel_test_mode')
const autoDisableParsed = useMemo(
() => parseHttpStatusCodeRules(autoDisableStatusCodes),
[autoDisableStatusCodes]
......@@ -353,6 +379,52 @@ export function RoutingReliabilitySection({
<FormField
control={form.control}
name='monitor_setting.channel_test_mode'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Channel test mode')}</FormLabel>
<Select
items={[
{
value: 'scheduled_all',
label: t('Scheduled full test'),
},
{
value: 'passive_recovery',
label: t('Passive recovery only'),
},
]}
value={field.value}
onValueChange={field.onChange}
>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent alignItemWithTrigger={false}>
<SelectGroup>
<SelectItem value='scheduled_all'>
{t('Scheduled full test')}
</SelectItem>
<SelectItem value='passive_recovery'>
{t('Passive recovery only')}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<FormDescription>
{t(
'Scheduled full test probes non-manually-disabled channels; passive recovery only checks auto-disabled channels after real request failures.'
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='monitor_setting.auto_test_channel_minutes'
render={({ field }) => (
<FormItem>
......@@ -366,7 +438,11 @@ export function RoutingReliabilitySection({
/>
</FormControl>
<FormDescription>
{t('How frequently the system tests all channels')}
{channelTestMode === 'passive_recovery'
? t(
'How frequently the system checks auto-disabled channels for recovery'
)
: t('How frequently the system tests all channels')}
</FormDescription>
<FormMessage />
</FormItem>
......
......@@ -83,6 +83,8 @@ const MODELS_SECTIONS = [
settings['monitor_setting.auto_test_channel_enabled'],
'monitor_setting.auto_test_channel_minutes':
settings['monitor_setting.auto_test_channel_minutes'],
'monitor_setting.channel_test_mode':
settings['monitor_setting.channel_test_mode'],
}}
/>
),
......
......@@ -183,6 +183,7 @@ export type ModelSettings = {
AutomaticRetryStatusCodes: string
'monitor_setting.auto_test_channel_enabled': boolean
'monitor_setting.auto_test_channel_minutes': number
'monitor_setting.channel_test_mode': 'scheduled_all' | 'passive_recovery'
'channel_affinity_setting.enabled': boolean
'channel_affinity_setting.switch_on_success': boolean
'channel_affinity_setting.keep_on_channel_disabled': boolean
......
......@@ -704,6 +704,7 @@
"Channel models": "Channel models",
"Channel name is required": "Channel name is required",
"Channel test completed": "Channel test completed",
"Channel test mode": "Channel test mode",
"Channel type is required": "Channel type is required",
"Channel updated successfully": "Channel updated successfully",
"Channel-specific settings (JSON format)": "Channel-specific settings (JSON format)",
......@@ -2078,6 +2079,7 @@
"Hourly token usage by model over the past 24 hours": "Hourly token usage by model over the past 24 hours",
"hours": "hours",
"How client credentials are sent to the token endpoint": "How client credentials are sent to the token endpoint",
"How frequently the system checks auto-disabled channels for recovery": "How frequently the system checks auto-disabled channels for recovery",
"How frequently the system tests all channels": "How frequently the system tests all channels",
"How It Works": "How It Works",
"How model mapping works": "How model mapping works",
......@@ -3014,6 +3016,7 @@
"Pass when key is missing": "Pass when key is missing",
"Pass-Through": "Pass-Through",
"Pass-through Headers (comma-separated or JSON array)": "Pass-through Headers (comma-separated or JSON array)",
"Passive recovery only": "Passive recovery only",
"Passkey": "Passkey",
"Passkey Authentication": "Passkey Authentication",
"Passkey is not available in this browser": "Passkey is not available in this browser",
......@@ -3690,6 +3693,8 @@
"Scan this QR code with your authenticator app (Google Authenticator, Microsoft Authenticator, etc.)": "Scan this QR code with your authenticator app (Google Authenticator, Microsoft Authenticator, etc.)",
"Scenario Templates": "Scenario Templates",
"Scheduled channel tests": "Scheduled channel tests",
"Scheduled full test": "Scheduled full test",
"Scheduled full test probes non-manually-disabled channels; passive recovery only checks auto-disabled channels after real request failures.": "Scheduled full test probes non-manually-disabled channels; passive recovery only checks auto-disabled channels after real request failures.",
"scheduling controls": "scheduling controls",
"Science": "Science",
"Scope": "Scope",
......
......@@ -704,6 +704,7 @@
"Channel models": "Modèles de canaux",
"Channel name is required": "Le nom du canal est requis",
"Channel test completed": "Test du canal terminé",
"Channel test mode": "Mode de test des canaux",
"Channel type is required": "Le type de canal est requis",
"Channel updated successfully": "Canal mis à jour avec succès",
"Channel-specific settings (JSON format)": "Paramètres spécifiques aux canaux (format JSON)",
......@@ -2078,6 +2079,7 @@
"Hourly token usage by model over the past 24 hours": "Utilisation horaire de tokens par modèle sur les dernières 24 heures",
"hours": "heures",
"How client credentials are sent to the token endpoint": "Comment les informations d'identification client sont envoyées au point de terminaison de jeton",
"How frequently the system checks auto-disabled channels for recovery": "Fréquence à laquelle le système vérifie la récupération des canaux désactivés automatiquement",
"How frequently the system tests all channels": "Fréquence à laquelle le système teste tous les canaux",
"How It Works": "Comment ça marche",
"How model mapping works": "Fonctionnement du mappage des modèles",
......@@ -3014,6 +3016,7 @@
"Pass when key is missing": "Accepter si la clé est absente",
"Pass-Through": "Transmission directe",
"Pass-through Headers (comma-separated or JSON array)": "En-têtes passthrough (séparés par des virgules ou tableau JSON)",
"Passive recovery only": "Récupération passive uniquement",
"Passkey": "Passkey",
"Passkey Authentication": "Authentification Passkey",
"Passkey is not available in this browser": "Passkey n'est pas disponible dans ce navigateur",
......@@ -3690,6 +3693,8 @@
"Scan this QR code with your authenticator app (Google Authenticator, Microsoft Authenticator, etc.)": "Scannez ce code QR avec votre application d'authentification (Google Authenticator, Microsoft Authenticator, etc.)",
"Scenario Templates": "Modèles de scénario",
"Scheduled channel tests": "Tests de canaux planifiés",
"Scheduled full test": "Test complet planifié",
"Scheduled full test probes non-manually-disabled channels; passive recovery only checks auto-disabled channels after real request failures.": "Le test complet planifié sonde les canaux non désactivés manuellement ; la récupération passive vérifie uniquement les canaux désactivés automatiquement après des échecs de requêtes réelles.",
"scheduling controls": "contrôles d'ordonnancement",
"Science": "Science",
"Scope": "Portée",
......
......@@ -704,6 +704,7 @@
"Channel models": "チャネルモデル",
"Channel name is required": "チャネル名が必要です",
"Channel test completed": "チャネルテストが完了しました",
"Channel test mode": "チャネルテストモード",
"Channel type is required": "チャネルタイプが必要です",
"Channel updated successfully": "チャネルが正常に更新されました",
"Channel-specific settings (JSON format)": "チャネル固有の設定 (JSON 形式)",
......@@ -2078,6 +2079,7 @@
"Hourly token usage by model over the past 24 hours": "過去24時間のモデル別時間単位のトークン使用量",
"hours": "時間",
"How client credentials are sent to the token endpoint": "クライアント認証情報がトークンエンドポイントに送信される方法",
"How frequently the system checks auto-disabled channels for recovery": "自動無効化されたチャネルの復旧を確認する頻度",
"How frequently the system tests all channels": "システムがすべてのチャネルをテストする頻度",
"How It Works": "仕組み",
"How model mapping works": "モデルマッピングの仕組み",
......@@ -3014,6 +3016,7 @@
"Pass when key is missing": "キーがない場合は通過",
"Pass-Through": "パススルー",
"Pass-through Headers (comma-separated or JSON array)": "パススルーヘッダー(カンマ区切りまたはJSON配列)",
"Passive recovery only": "パッシブ復旧のみ",
"Passkey": "Passkey",
"Passkey Authentication": "パスキー認証",
"Passkey is not available in this browser": "このブラウザではパスキーが利用できません",
......@@ -3690,6 +3693,8 @@
"Scan this QR code with your authenticator app (Google Authenticator, Microsoft Authenticator, etc.)": "このQRコードを認証アプリ(Google Authenticator、Microsoft Authenticatorなど)でスキャンしてください。",
"Scenario Templates": "シナリオテンプレート",
"Scheduled channel tests": "スケジュールされたチャネルテスト",
"Scheduled full test": "定期フルテスト",
"Scheduled full test probes non-manually-disabled channels; passive recovery only checks auto-disabled channels after real request failures.": "定期フルテストは手動で無効化されていないチャネルを検査します。パッシブ復旧は実リクエストの失敗後に自動無効化されたチャネルのみを確認します。",
"scheduling controls": "スケジューリング制御",
"Science": "科学",
"Scope": "スコープ",
......
......@@ -704,6 +704,7 @@
"Channel models": "Модели каналов",
"Channel name is required": "Имя канала обязательно",
"Channel test completed": "Тест канала завершён",
"Channel test mode": "Режим проверки каналов",
"Channel type is required": "Тип канала обязателен",
"Channel updated successfully": "Канал успешно обновлён",
"Channel-specific settings (JSON format)": "Настройки, специфичные для канала (формат JSON)",
......@@ -2078,6 +2079,7 @@
"Hourly token usage by model over the past 24 hours": "Почасовое использование токенов по моделям за последние 24 часа",
"hours": "часов",
"How client credentials are sent to the token endpoint": "Как учетные данные клиента отправляются на конечную точку токена",
"How frequently the system checks auto-disabled channels for recovery": "Как часто система проверяет автоматически отключенные каналы для восстановления",
"How frequently the system tests all channels": "Как часто система тестирует все каналы",
"How It Works": "Как это работает",
"How model mapping works": "Как работает сопоставление моделей",
......@@ -3014,6 +3016,7 @@
"Pass when key is missing": "Пропускать при отсутствии ключа",
"Pass-Through": "Сквозной доступ",
"Pass-through Headers (comma-separated or JSON array)": "Пробрасываемые заголовки (через запятую или JSON-массив)",
"Passive recovery only": "Только пассивное восстановление",
"Passkey": "Passkey",
"Passkey Authentication": "Аутентификация Passkey",
"Passkey is not available in this browser": "Passkey недоступен в этом браузере",
......@@ -3690,6 +3693,8 @@
"Scan this QR code with your authenticator app (Google Authenticator, Microsoft Authenticator, etc.)": "Отсканируйте этот QR-код с помощью вашего приложения-аутентификатора (Google Authenticator, Microsoft Authenticator и т.д.)",
"Scenario Templates": "Шаблоны сценариев",
"Scheduled channel tests": "Запланированные тесты канала",
"Scheduled full test": "Полная проверка по расписанию",
"Scheduled full test probes non-manually-disabled channels; passive recovery only checks auto-disabled channels after real request failures.": "Полная проверка по расписанию проверяет каналы, не отключенные вручную; пассивное восстановление проверяет только автоматически отключенные каналы после сбоев реальных запросов.",
"scheduling controls": "механизмов управления маршрутизацией",
"Science": "Наука",
"Scope": "Область",
......
......@@ -704,6 +704,7 @@
"Channel models": "Mô hình kênh",
"Channel name is required": "Tên kênh là bắt buộc",
"Channel test completed": "Kiểm tra kênh hoàn tất",
"Channel test mode": "Chế độ kiểm tra kênh",
"Channel type is required": "Loại kênh là bắt buộc",
"Channel updated successfully": "Kênh đã được cập nhật thành công",
"Channel-specific settings (JSON format)": "Cài đặt dành riêng cho kênh (định dạng JSON)",
......@@ -2078,6 +2079,7 @@
"Hourly token usage by model over the past 24 hours": "Sử dụng token theo mô hình theo giờ trong 24 giờ qua",
"hours": "giờ",
"How client credentials are sent to the token endpoint": "Cách thông tin xác thực client được gửi đến endpoint token",
"How frequently the system checks auto-disabled channels for recovery": "Tần suất hệ thống kiểm tra các kênh bị tự động vô hiệu hóa để khôi phục",
"How frequently the system tests all channels": "Tần suất hệ thống kiểm tra tất cả các kênh là bao nhiêu?",
"How It Works": "Cách hoạt động",
"How model mapping works": "Cách hoạt động của ánh xạ mô hình",
......@@ -3014,6 +3016,7 @@
"Pass when key is missing": "Cho qua khi thiếu khóa",
"Pass-Through": "Chuyển tiếp",
"Pass-through Headers (comma-separated or JSON array)": "Header chuyển tiếp (phân cách bằng dấu phẩy hoặc mảng JSON)",
"Passive recovery only": "Chỉ khôi phục thụ động",
"Passkey": "Khóa truy cập",
"Passkey Authentication": "Xác thực khóa truy cập",
"Passkey is not available in this browser": "Passkey không khả dụng trong trình duyệt này",
......@@ -3690,6 +3693,8 @@
"Scan this QR code with your authenticator app (Google Authenticator, Microsoft Authenticator, etc.)": "Quét mã QR này bằng ứng dụng xác thực của bạn (Google Authenticator, Microsoft Authenticator, v.v.)",
"Scenario Templates": "Mẫu kịch bản",
"Scheduled channel tests": "Kiểm tra kênh theo lịch trình",
"Scheduled full test": "Kiểm tra toàn bộ theo lịch",
"Scheduled full test probes non-manually-disabled channels; passive recovery only checks auto-disabled channels after real request failures.": "Kiểm tra toàn bộ theo lịch sẽ dò các kênh không bị vô hiệu hóa thủ công; khôi phục thụ động chỉ kiểm tra các kênh bị tự động vô hiệu hóa sau lỗi từ yêu cầu thực tế.",
"scheduling controls": "điều khiển điều phối",
"Science": "Khoa học",
"Scope": "Phạm vi",
......
......@@ -704,6 +704,7 @@
"Channel models": "渠道模型",
"Channel name is required": "渠道名称是必填的",
"Channel test completed": "渠道测试完成",
"Channel test mode": "渠道测试模式",
"Channel type is required": "渠道类型是必填的",
"Channel updated successfully": "渠道更新成功",
"Channel-specific settings (JSON format)": "渠道特定设置(JSON 格式)",
......@@ -2078,6 +2079,7 @@
"Hourly token usage by model over the past 24 hours": "过去 24 小时内按模型分布的小时级 Token 使用量",
"hours": "小时",
"How client credentials are sent to the token endpoint": "客户端凭据如何发送至令牌端点",
"How frequently the system checks auto-disabled channels for recovery": "系统检查自动禁用渠道是否可恢复的频率",
"How frequently the system tests all channels": "系统测试所有渠道的频率",
"How It Works": "工作流程",
"How model mapping works": "模型映射如何工作",
......@@ -3014,6 +3016,7 @@
"Pass when key is missing": "字段缺失时通过",
"Pass-Through": "透传",
"Pass-through Headers (comma-separated or JSON array)": "透传请求头(逗号分隔或 JSON 数组)",
"Passive recovery only": "仅被动恢复",
"Passkey": "Passkey",
"Passkey Authentication": "通行密钥认证",
"Passkey is not available in this browser": "此浏览器中不支持 Passkey",
......@@ -3690,6 +3693,8 @@
"Scan this QR code with your authenticator app (Google Authenticator, Microsoft Authenticator, etc.)": "使用您的身份验证器应用(Google Authenticator、Microsoft Authenticator 等)扫描此二维码",
"Scenario Templates": "场景模板",
"Scheduled channel tests": "定期渠道测试",
"Scheduled full test": "定时全量测试",
"Scheduled full test probes non-manually-disabled channels; passive recovery only checks auto-disabled channels after real request failures.": "定时全量测试会探测非手动禁用的渠道;仅被动恢复只会在真实请求失败导致渠道自动禁用后检查这些渠道是否可恢复。",
"scheduling controls": "调度控制能力",
"Science": "科研",
"Scope": "作用域",
......
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