+149
-72
@@ -12,6 +12,7 @@ import (
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
@@ -908,92 +909,167 @@ type channelTestSummary struct {
|
||||
Enabled int `json:"enabled"`
|
||||
}
|
||||
|
||||
// performChannelTests runs the channel test loop synchronously, honoring ctx
|
||||
// cancellation so a system-task runner that loses its lease stops promptly. When
|
||||
// report is non-nil it is called after each channel with (processed, total) so
|
||||
// the system task can surface progress.
|
||||
func performChannelTests(ctx context.Context, channels []*model.Channel, testUserID int, allowDisable bool, report func(processed, total int)) channelTestSummary {
|
||||
func testChannelForHealthCheck(ctx context.Context, channel *model.Channel, testUserID int, allowDisable bool, disableThreshold int64) channelTestSummary {
|
||||
summary := channelTestSummary{}
|
||||
var disableThreshold = int64(common.ChannelDisableThreshold * 1000)
|
||||
if disableThreshold == 0 {
|
||||
disableThreshold = 10000000 // a impossible value
|
||||
isChannelEnabled := channel.Status == common.ChannelStatusEnabled
|
||||
tik := time.Now()
|
||||
result := testChannel(ctx, channel, testUserID, "", "", shouldUseStreamForAutomaticChannelTest(channel))
|
||||
milliseconds := time.Since(tik).Milliseconds()
|
||||
if ctx.Err() != nil {
|
||||
return summary
|
||||
}
|
||||
|
||||
summary.Tested++
|
||||
|
||||
shouldBanChannel := false
|
||||
newAPIError := result.newAPIError
|
||||
if newAPIError != nil {
|
||||
shouldBanChannel = service.ShouldDisableChannel(result.newAPIError)
|
||||
}
|
||||
|
||||
if common.AutomaticDisableChannelEnabled && !shouldBanChannel {
|
||||
if milliseconds > disableThreshold {
|
||||
err := fmt.Errorf("响应时间 %.2fs 超过阈值 %.2fs", float64(milliseconds)/1000.0, float64(disableThreshold)/1000.0)
|
||||
newAPIError = types.NewOpenAIError(err, types.ErrorCodeChannelResponseTimeExceeded, http.StatusRequestTimeout)
|
||||
shouldBanChannel = true
|
||||
}
|
||||
}
|
||||
|
||||
if newAPIError == nil {
|
||||
summary.Succeeded++
|
||||
} else {
|
||||
summary.Failed++
|
||||
}
|
||||
|
||||
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)
|
||||
summary.Disabled++
|
||||
}
|
||||
|
||||
if result.localErr == nil && !isChannelEnabled && service.ShouldEnableChannel(newAPIError, channel.Status) {
|
||||
service.EnableChannel(channel.Id, common.GetContextKeyString(result.context, constant.ContextKeyChannelKey), channel.Name)
|
||||
summary.Enabled++
|
||||
}
|
||||
|
||||
channel.UpdateResponseTime(milliseconds)
|
||||
return summary
|
||||
}
|
||||
|
||||
// runChannelTestWorkers executes independent channel tests with bounded
|
||||
// concurrency. Results and progress are reduced by the caller goroutine, so
|
||||
// summary counts and the progress reporter remain serialized.
|
||||
func runChannelTestWorkers(
|
||||
ctx context.Context,
|
||||
channels []*model.Channel,
|
||||
concurrency int,
|
||||
run func(context.Context, *model.Channel) channelTestSummary,
|
||||
report func(processed, total int),
|
||||
) channelTestSummary {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
total := len(channels)
|
||||
for index, channel := range channels {
|
||||
if ctx != nil && ctx.Err() != nil {
|
||||
break
|
||||
}
|
||||
if report != nil {
|
||||
report(index, total) // channels completed before this one
|
||||
}
|
||||
if channel.Status == common.ChannelStatusManuallyDisabled {
|
||||
continue
|
||||
}
|
||||
isChannelEnabled := channel.Status == common.ChannelStatusEnabled
|
||||
tik := time.Now()
|
||||
result := testChannel(ctx, channel, testUserID, "", "", shouldUseStreamForAutomaticChannelTest(channel))
|
||||
tok := time.Now()
|
||||
milliseconds := tok.Sub(tik).Milliseconds()
|
||||
if ctx != nil && ctx.Err() != nil {
|
||||
break
|
||||
}
|
||||
if report != nil {
|
||||
report(0, total)
|
||||
}
|
||||
if total == 0 {
|
||||
return channelTestSummary{}
|
||||
}
|
||||
|
||||
summary.Tested++
|
||||
workerCount := min(operation_setting.NormalizeChannelTestConcurrency(concurrency), total)
|
||||
jobs := make(chan *model.Channel)
|
||||
results := make(chan channelTestSummary)
|
||||
|
||||
shouldBanChannel := false
|
||||
newAPIError := result.newAPIError
|
||||
// request error disables the channel
|
||||
if newAPIError != nil {
|
||||
shouldBanChannel = service.ShouldDisableChannel(result.newAPIError)
|
||||
}
|
||||
|
||||
// 当错误检查通过,才检查响应时间
|
||||
if common.AutomaticDisableChannelEnabled && !shouldBanChannel {
|
||||
if milliseconds > disableThreshold {
|
||||
err := fmt.Errorf("响应时间 %.2fs 超过阈值 %.2fs", float64(milliseconds)/1000.0, float64(disableThreshold)/1000.0)
|
||||
newAPIError = types.NewOpenAIError(err, types.ErrorCodeChannelResponseTimeExceeded, http.StatusRequestTimeout)
|
||||
shouldBanChannel = true
|
||||
}
|
||||
}
|
||||
|
||||
if newAPIError == nil {
|
||||
summary.Succeeded++
|
||||
} else {
|
||||
summary.Failed++
|
||||
}
|
||||
|
||||
// disable channel
|
||||
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)
|
||||
summary.Disabled++
|
||||
}
|
||||
|
||||
// enable channel
|
||||
if result.localErr == nil && !isChannelEnabled && service.ShouldEnableChannel(newAPIError, channel.Status) {
|
||||
service.EnableChannel(channel.Id, common.GetContextKeyString(result.context, constant.ContextKeyChannelKey), channel.Name)
|
||||
summary.Enabled++
|
||||
}
|
||||
|
||||
channel.UpdateResponseTime(milliseconds)
|
||||
if common.RequestInterval > 0 {
|
||||
if ctx == nil {
|
||||
time.Sleep(common.RequestInterval)
|
||||
} else {
|
||||
var workers sync.WaitGroup
|
||||
workers.Add(workerCount)
|
||||
for range workerCount {
|
||||
go func() {
|
||||
defer workers.Done()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return summary
|
||||
case <-time.After(common.RequestInterval):
|
||||
return
|
||||
case channel, ok := <-jobs:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
|
||||
result := channelTestSummary{}
|
||||
if channel != nil && channel.Status != common.ChannelStatusManuallyDisabled {
|
||||
result = run(ctx, channel)
|
||||
}
|
||||
|
||||
results <- result
|
||||
|
||||
if common.RequestInterval > 0 {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(common.RequestInterval):
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer close(jobs)
|
||||
for _, channel := range channels {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case jobs <- channel:
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
workers.Wait()
|
||||
close(results)
|
||||
}()
|
||||
|
||||
summary := channelTestSummary{}
|
||||
processed := 0
|
||||
for result := range results {
|
||||
summary.Tested += result.Tested
|
||||
summary.Succeeded += result.Succeeded
|
||||
summary.Failed += result.Failed
|
||||
summary.Disabled += result.Disabled
|
||||
summary.Enabled += result.Enabled
|
||||
processed++
|
||||
if report != nil && ctx.Err() == nil {
|
||||
report(processed, total)
|
||||
}
|
||||
}
|
||||
if report != nil && (ctx == nil || ctx.Err() == nil) {
|
||||
report(total, total) // mark complete only when the full set was tested
|
||||
}
|
||||
return summary
|
||||
}
|
||||
|
||||
// performChannelTests runs channel health checks with the configured bounded
|
||||
// concurrency and honors cancellation when a system-task runner loses its
|
||||
// lease.
|
||||
func performChannelTests(ctx context.Context, channels []*model.Channel, testUserID int, allowDisable bool, concurrency int, report func(processed, total int)) channelTestSummary {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
disableThreshold := int64(common.ChannelDisableThreshold * 1000)
|
||||
if disableThreshold == 0 {
|
||||
disableThreshold = 10000000 // an impossible value
|
||||
}
|
||||
return runChannelTestWorkers(
|
||||
ctx,
|
||||
channels,
|
||||
concurrency,
|
||||
func(ctx context.Context, channel *model.Channel) channelTestSummary {
|
||||
return testChannelForHealthCheck(ctx, channel, testUserID, allowDisable, disableThreshold)
|
||||
},
|
||||
report,
|
||||
)
|
||||
}
|
||||
|
||||
// runChannelTestTask runs one synchronous channel test cycle for the system task
|
||||
// runner (both the scheduled job and the manual "test all channels" trigger go
|
||||
// through here). It honors ctx cancellation so a runner that loses its lease
|
||||
@@ -1016,7 +1092,8 @@ func runChannelTestTask(ctx context.Context, mode string, notify bool, report fu
|
||||
}
|
||||
selected := selectChannelsForAutomaticTest(channels, mode)
|
||||
allowDisable := mode != operation_setting.ChannelTestModePassiveRecovery
|
||||
summary := performChannelTests(ctx, selected, testUserID, allowDisable, report)
|
||||
concurrency := operation_setting.GetMonitorSetting().ChannelTestConcurrency
|
||||
summary := performChannelTests(ctx, selected, testUserID, allowDisable, concurrency, report)
|
||||
if notify && (ctx == nil || ctx.Err() == nil) {
|
||||
service.NotifyRootUser(dto.NotifyTypeChannelTest, "通道测试完成", "所有通道测试已完成")
|
||||
}
|
||||
|
||||
@@ -2,9 +2,11 @@ package controller
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
@@ -339,6 +341,111 @@ func TestSelectChannelsForAutomaticTestAutoBanOnlyUsesEligibleChannels(t *testin
|
||||
require.Equal(t, 3, selected[1].Id)
|
||||
}
|
||||
|
||||
func TestRunChannelTestWorkersHonorsConfiguredConcurrency(t *testing.T) {
|
||||
originalInterval := common.RequestInterval
|
||||
common.RequestInterval = 0
|
||||
t.Cleanup(func() { common.RequestInterval = originalInterval })
|
||||
|
||||
channels := []*model.Channel{
|
||||
{Id: 1, Status: common.ChannelStatusEnabled},
|
||||
{Id: 2, Status: common.ChannelStatusEnabled},
|
||||
{Id: 3, Status: common.ChannelStatusEnabled},
|
||||
{Id: 4, Status: common.ChannelStatusEnabled},
|
||||
}
|
||||
started := make(chan struct{}, len(channels))
|
||||
release := make(chan struct{})
|
||||
var active atomic.Int32
|
||||
var maxActive atomic.Int32
|
||||
progress := make([]int, 0, len(channels)+1)
|
||||
summaryResult := make(chan channelTestSummary, 1)
|
||||
|
||||
go func() {
|
||||
summaryResult <- runChannelTestWorkers(
|
||||
context.Background(),
|
||||
channels,
|
||||
2,
|
||||
func(_ context.Context, _ *model.Channel) channelTestSummary {
|
||||
current := active.Add(1)
|
||||
defer active.Add(-1)
|
||||
for {
|
||||
observed := maxActive.Load()
|
||||
if current <= observed || maxActive.CompareAndSwap(observed, current) {
|
||||
break
|
||||
}
|
||||
}
|
||||
started <- struct{}{}
|
||||
<-release
|
||||
return channelTestSummary{Tested: 1, Succeeded: 1}
|
||||
},
|
||||
func(processed, _ int) {
|
||||
progress = append(progress, processed)
|
||||
},
|
||||
)
|
||||
}()
|
||||
|
||||
<-started
|
||||
<-started
|
||||
select {
|
||||
case <-started:
|
||||
t.Fatal("started more channel tests than the configured concurrency")
|
||||
default:
|
||||
}
|
||||
close(release)
|
||||
|
||||
summary := <-summaryResult
|
||||
|
||||
assert.Equal(t, int32(2), maxActive.Load())
|
||||
assert.Equal(t, channelTestSummary{Tested: 4, Succeeded: 4}, summary)
|
||||
assert.Equal(t, []int{0, 1, 2, 3, 4}, progress)
|
||||
}
|
||||
|
||||
func TestRunChannelTestWorkersStopsAfterCancellation(t *testing.T) {
|
||||
originalInterval := common.RequestInterval
|
||||
common.RequestInterval = 0
|
||||
t.Cleanup(func() { common.RequestInterval = originalInterval })
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
channels := []*model.Channel{
|
||||
{Id: 1, Status: common.ChannelStatusEnabled},
|
||||
{Id: 2, Status: common.ChannelStatusEnabled},
|
||||
{Id: 3, Status: common.ChannelStatusEnabled},
|
||||
{Id: 4, Status: common.ChannelStatusEnabled},
|
||||
}
|
||||
started := make(chan struct{}, len(channels))
|
||||
progress := make([]int, 0, 1)
|
||||
summaryResult := make(chan channelTestSummary, 1)
|
||||
|
||||
go func() {
|
||||
summaryResult <- runChannelTestWorkers(
|
||||
ctx,
|
||||
channels,
|
||||
2,
|
||||
func(ctx context.Context, _ *model.Channel) channelTestSummary {
|
||||
started <- struct{}{}
|
||||
<-ctx.Done()
|
||||
return channelTestSummary{Tested: 1, Succeeded: 1}
|
||||
},
|
||||
func(processed, _ int) {
|
||||
progress = append(progress, processed)
|
||||
},
|
||||
)
|
||||
}()
|
||||
|
||||
<-started
|
||||
<-started
|
||||
cancel()
|
||||
|
||||
summary := <-summaryResult
|
||||
|
||||
select {
|
||||
case <-started:
|
||||
t.Fatal("started another channel test after cancellation")
|
||||
default:
|
||||
}
|
||||
assert.Equal(t, channelTestSummary{Tested: 2, Succeeded: 2}, summary)
|
||||
assert.Equal(t, []int{0}, progress)
|
||||
}
|
||||
|
||||
func TestTestAllChannelsRejectsExistingActiveTask(t *testing.T) {
|
||||
db := setupModelListControllerTestDB(t)
|
||||
require.NoError(t, db.AutoMigrate(&model.SystemTask{}, &model.SystemTaskLock{}))
|
||||
|
||||
@@ -209,6 +209,9 @@ func validateOptionValue(key string, value string) error {
|
||||
if key == operation_setting.ToolPriceOptionKey {
|
||||
return operation_setting.ValidateToolPricesJSON(value)
|
||||
}
|
||||
if key == operation_setting.ChannelTestConcurrencyOptionKey {
|
||||
return operation_setting.ValidateChannelTestConcurrency(value)
|
||||
}
|
||||
if key == "MaxTokenAutoGroups" {
|
||||
return setting.ValidateMaxTokenAutoGroups(value)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package operation_setting
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
@@ -11,12 +12,17 @@ type MonitorSetting struct {
|
||||
AutoTestChannelEnabled bool `json:"auto_test_channel_enabled"`
|
||||
AutoTestChannelMinutes float64 `json:"auto_test_channel_minutes"`
|
||||
ChannelTestMode string `json:"channel_test_mode"`
|
||||
ChannelTestConcurrency int `json:"channel_test_concurrency"`
|
||||
}
|
||||
|
||||
const (
|
||||
ChannelTestModeScheduledAll = "scheduled_all"
|
||||
ChannelTestModeAutoBanOnly = "auto_ban_only"
|
||||
ChannelTestModePassiveRecovery = "passive_recovery"
|
||||
|
||||
ChannelTestConcurrencyOptionKey = "monitor_setting.channel_test_concurrency"
|
||||
DefaultChannelTestConcurrency = 1
|
||||
MaxChannelTestConcurrency = 32
|
||||
)
|
||||
|
||||
// 默认配置
|
||||
@@ -24,6 +30,7 @@ var monitorSetting = MonitorSetting{
|
||||
AutoTestChannelEnabled: false,
|
||||
AutoTestChannelMinutes: 10,
|
||||
ChannelTestMode: ChannelTestModeScheduledAll,
|
||||
ChannelTestConcurrency: DefaultChannelTestConcurrency,
|
||||
}
|
||||
|
||||
func init() {
|
||||
@@ -51,5 +58,24 @@ func GetMonitorSetting() *MonitorSetting {
|
||||
default:
|
||||
monitorSetting.ChannelTestMode = ChannelTestModeScheduledAll
|
||||
}
|
||||
monitorSetting.ChannelTestConcurrency = NormalizeChannelTestConcurrency(monitorSetting.ChannelTestConcurrency)
|
||||
return &monitorSetting
|
||||
}
|
||||
|
||||
func NormalizeChannelTestConcurrency(concurrency int) int {
|
||||
if concurrency < 1 {
|
||||
return DefaultChannelTestConcurrency
|
||||
}
|
||||
if concurrency > MaxChannelTestConcurrency {
|
||||
return MaxChannelTestConcurrency
|
||||
}
|
||||
return concurrency
|
||||
}
|
||||
|
||||
func ValidateChannelTestConcurrency(value string) error {
|
||||
concurrency, err := strconv.Atoi(value)
|
||||
if err != nil || concurrency < 1 || concurrency > MaxChannelTestConcurrency {
|
||||
return fmt.Errorf("channel test concurrency must be between 1 and %d", MaxChannelTestConcurrency)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -55,3 +55,37 @@ func TestGetMonitorSettingPreservesAutoBanOnlyMode(t *testing.T) {
|
||||
require.NotNil(t, setting)
|
||||
assert.Equal(t, ChannelTestModeAutoBanOnly, setting.ChannelTestMode)
|
||||
}
|
||||
|
||||
func TestGetMonitorSettingNormalizesChannelTestConcurrency(t *testing.T) {
|
||||
orig := monitorSetting
|
||||
t.Cleanup(func() { monitorSetting = orig })
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
concurrency int
|
||||
want int
|
||||
}{
|
||||
{name: "missing uses safe default", concurrency: 0, want: DefaultChannelTestConcurrency},
|
||||
{name: "configured value is preserved", concurrency: 8, want: 8},
|
||||
{name: "oversized value is capped", concurrency: MaxChannelTestConcurrency + 1, want: MaxChannelTestConcurrency},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
monitorSetting = MonitorSetting{ChannelTestConcurrency: test.concurrency}
|
||||
|
||||
setting := GetMonitorSetting()
|
||||
|
||||
require.NotNil(t, setting)
|
||||
assert.Equal(t, test.want, setting.ChannelTestConcurrency)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateChannelTestConcurrency(t *testing.T) {
|
||||
require.NoError(t, ValidateChannelTestConcurrency("1"))
|
||||
require.NoError(t, ValidateChannelTestConcurrency("32"))
|
||||
assert.Error(t, ValidateChannelTestConcurrency("0"))
|
||||
assert.Error(t, ValidateChannelTestConcurrency("33"))
|
||||
assert.Error(t, ValidateChannelTestConcurrency("1.5"))
|
||||
}
|
||||
|
||||
@@ -335,6 +335,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_concurrency': 1,
|
||||
'monitor_setting.channel_test_mode': 'scheduled_all',
|
||||
'channel_affinity_setting.enabled': false,
|
||||
'channel_affinity_setting.switch_on_success': true,
|
||||
|
||||
@@ -73,6 +73,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_concurrency': 1,
|
||||
'monitor_setting.channel_test_mode': 'scheduled_all',
|
||||
'channel_affinity_setting.enabled': false,
|
||||
'channel_affinity_setting.switch_on_success': true,
|
||||
|
||||
@@ -69,55 +69,70 @@ const channelTestModes = [
|
||||
'passive_recovery',
|
||||
] as const
|
||||
type ChannelTestMode = (typeof channelTestModes)[number]
|
||||
const MAX_CHANNEL_TEST_CONCURRENCY = 32
|
||||
|
||||
const routingReliabilitySchema = z
|
||||
.object({
|
||||
RetryTimes: z.coerce.number().min(0).max(10),
|
||||
ChannelDisableThreshold: numericString,
|
||||
AutomaticDisableChannelEnabled: z.boolean(),
|
||||
AutomaticEnableChannelEnabled: z.boolean(),
|
||||
AutomaticDisableKeywords: z.string(),
|
||||
AutomaticDisableStatusCodes: z.string(),
|
||||
AutomaticRetryStatusCodes: z.string(),
|
||||
monitor_setting: z.object({
|
||||
auto_test_channel_enabled: z.boolean(),
|
||||
auto_test_channel_minutes: z.coerce
|
||||
.number()
|
||||
.int()
|
||||
.min(1, 'Interval must be at least 1 minute'),
|
||||
channel_test_mode: z.enum(channelTestModes),
|
||||
}),
|
||||
})
|
||||
.superRefine((values, ctx) => {
|
||||
const disableParsed = parseHttpStatusCodeRules(
|
||||
values.AutomaticDisableStatusCodes
|
||||
)
|
||||
if (!disableParsed.ok) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
path: ['AutomaticDisableStatusCodes'],
|
||||
message: `Invalid status code rules: ${disableParsed.invalidTokens.join(
|
||||
', '
|
||||
)}`,
|
||||
})
|
||||
}
|
||||
const createRoutingReliabilitySchema = (
|
||||
t: (key: string, options?: Record<string, unknown>) => string
|
||||
) =>
|
||||
z
|
||||
.object({
|
||||
RetryTimes: z.coerce.number().min(0).max(10),
|
||||
ChannelDisableThreshold: numericString,
|
||||
AutomaticDisableChannelEnabled: z.boolean(),
|
||||
AutomaticEnableChannelEnabled: z.boolean(),
|
||||
AutomaticDisableKeywords: z.string(),
|
||||
AutomaticDisableStatusCodes: z.string(),
|
||||
AutomaticRetryStatusCodes: z.string(),
|
||||
monitor_setting: z.object({
|
||||
auto_test_channel_enabled: z.boolean(),
|
||||
auto_test_channel_minutes: z.coerce
|
||||
.number()
|
||||
.int()
|
||||
.min(1, t('Interval must be at least 1 minute')),
|
||||
channel_test_concurrency: z.coerce
|
||||
.number()
|
||||
.int(t('Enter a positive integer'))
|
||||
.min(1, t('Channel test concurrency must be between 1 and 32'))
|
||||
.max(
|
||||
MAX_CHANNEL_TEST_CONCURRENCY,
|
||||
t('Channel test concurrency must be between 1 and 32')
|
||||
),
|
||||
channel_test_mode: z.enum(channelTestModes),
|
||||
}),
|
||||
})
|
||||
.superRefine((values, ctx) => {
|
||||
const disableParsed = parseHttpStatusCodeRules(
|
||||
values.AutomaticDisableStatusCodes
|
||||
)
|
||||
if (!disableParsed.ok) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
path: ['AutomaticDisableStatusCodes'],
|
||||
message: t('Invalid status code rules: {{tokens}}', {
|
||||
tokens: disableParsed.invalidTokens.join(', '),
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
const retryParsed = parseHttpStatusCodeRules(
|
||||
values.AutomaticRetryStatusCodes
|
||||
)
|
||||
if (!retryParsed.ok) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
path: ['AutomaticRetryStatusCodes'],
|
||||
message: `Invalid status code rules: ${retryParsed.invalidTokens.join(
|
||||
', '
|
||||
)}`,
|
||||
})
|
||||
}
|
||||
})
|
||||
const retryParsed = parseHttpStatusCodeRules(
|
||||
values.AutomaticRetryStatusCodes
|
||||
)
|
||||
if (!retryParsed.ok) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
path: ['AutomaticRetryStatusCodes'],
|
||||
message: t('Invalid status code rules: {{tokens}}', {
|
||||
tokens: retryParsed.invalidTokens.join(', '),
|
||||
}),
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
type RoutingReliabilityFormValues = z.output<typeof routingReliabilitySchema>
|
||||
type RoutingReliabilityFormInput = z.input<typeof routingReliabilitySchema>
|
||||
type RoutingReliabilitySchema = ReturnType<
|
||||
typeof createRoutingReliabilitySchema
|
||||
>
|
||||
type RoutingReliabilityFormValues = z.output<RoutingReliabilitySchema>
|
||||
type RoutingReliabilityFormInput = z.input<RoutingReliabilitySchema>
|
||||
|
||||
type RoutingReliabilitySectionProps = {
|
||||
defaultValues: {
|
||||
@@ -130,6 +145,7 @@ type RoutingReliabilitySectionProps = {
|
||||
AutomaticRetryStatusCodes: string
|
||||
'monitor_setting.auto_test_channel_enabled': boolean
|
||||
'monitor_setting.auto_test_channel_minutes': number
|
||||
'monitor_setting.channel_test_concurrency': number
|
||||
'monitor_setting.channel_test_mode': ChannelTestMode
|
||||
}
|
||||
}
|
||||
@@ -148,6 +164,7 @@ type NormalizedRoutingReliabilityValues = {
|
||||
AutomaticRetryStatusCodes: string
|
||||
'monitor_setting.auto_test_channel_enabled': boolean
|
||||
'monitor_setting.auto_test_channel_minutes': number
|
||||
'monitor_setting.channel_test_concurrency': number
|
||||
'monitor_setting.channel_test_mode': ChannelTestMode
|
||||
}
|
||||
|
||||
@@ -175,6 +192,8 @@ const buildFormDefaults = (
|
||||
defaults['monitor_setting.auto_test_channel_enabled'],
|
||||
auto_test_channel_minutes:
|
||||
defaults['monitor_setting.auto_test_channel_minutes'],
|
||||
channel_test_concurrency:
|
||||
defaults['monitor_setting.channel_test_concurrency'],
|
||||
channel_test_mode: normalizeChannelTestMode(
|
||||
defaults['monitor_setting.channel_test_mode']
|
||||
),
|
||||
@@ -201,6 +220,8 @@ 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_concurrency':
|
||||
defaults['monitor_setting.channel_test_concurrency'],
|
||||
'monitor_setting.channel_test_mode': normalizeChannelTestMode(
|
||||
defaults['monitor_setting.channel_test_mode']
|
||||
),
|
||||
@@ -226,6 +247,8 @@ 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_concurrency':
|
||||
values.monitor_setting.channel_test_concurrency,
|
||||
'monitor_setting.channel_test_mode': values.monitor_setting.channel_test_mode,
|
||||
})
|
||||
|
||||
@@ -234,6 +257,7 @@ export function RoutingReliabilitySection({
|
||||
}: RoutingReliabilitySectionProps) {
|
||||
const { t } = useTranslation()
|
||||
const updateOption = useUpdateOption()
|
||||
const routingReliabilitySchema = createRoutingReliabilitySchema(t)
|
||||
const baselineRef = useRef<NormalizedRoutingReliabilityValues>(
|
||||
normalizeDefaults(defaultValues)
|
||||
)
|
||||
@@ -484,6 +508,31 @@ export function RoutingReliabilitySection({
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='monitor_setting.channel_test_concurrency'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('Channel test concurrency')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type='number'
|
||||
min={1}
|
||||
max={MAX_CHANNEL_TEST_CONCURRENCY}
|
||||
step={1}
|
||||
{...safeNumberFieldProps(field)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t(
|
||||
'Maximum number of channels tested at the same time (1-32)'
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='AutomaticEnableChannelEnabled'
|
||||
|
||||
@@ -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_concurrency':
|
||||
settings['monitor_setting.channel_test_concurrency'],
|
||||
'monitor_setting.channel_test_mode':
|
||||
settings['monitor_setting.channel_test_mode'],
|
||||
}}
|
||||
|
||||
@@ -235,6 +235,7 @@ export type ModelSettings = {
|
||||
AutomaticRetryStatusCodes: string
|
||||
'monitor_setting.auto_test_channel_enabled': boolean
|
||||
'monitor_setting.auto_test_channel_minutes': number
|
||||
'monitor_setting.channel_test_concurrency': number
|
||||
'monitor_setting.channel_test_mode':
|
||||
| 'scheduled_all'
|
||||
| 'auto_ban_only'
|
||||
|
||||
@@ -776,6 +776,8 @@
|
||||
"Channel models": "Channel models",
|
||||
"Channel name is required": "Channel name is required",
|
||||
"Channel test completed": "Channel test completed",
|
||||
"Channel test concurrency": "Channel test concurrency",
|
||||
"Channel test concurrency must be between 1 and 32": "Channel test concurrency must be between 1 and 32",
|
||||
"Channel test mode": "Channel test mode",
|
||||
"Channel type is required": "Channel type is required",
|
||||
"Channel updated successfully": "Channel updated successfully",
|
||||
@@ -2377,6 +2379,7 @@
|
||||
"Internal Notes": "Internal Notes",
|
||||
"Internal notes (not shown to users)": "Internal notes (not shown to users)",
|
||||
"Internal Server Error!": "Internal Server Error!",
|
||||
"Interval must be at least 1 minute": "Interval must be at least 1 minute",
|
||||
"Invalid (NaN)": "Invalid (NaN)",
|
||||
"Invalid chat link. Please contact the administrator.": "Invalid chat link. Please contact the administrator.",
|
||||
"Invalid chat link. Please contact your administrator.": "Invalid chat link. Please contact your administrator.",
|
||||
@@ -2394,6 +2397,7 @@
|
||||
"Invalid reset link, please request a new password reset.": "Invalid reset link, please request a new password reset.",
|
||||
"Invalid rules JSON format": "Invalid rules JSON format",
|
||||
"Invalid status code mapping entries: {{entries}}": "Invalid status code mapping entries: {{entries}}",
|
||||
"Invalid status code rules: {{tokens}}": "Invalid status code rules: {{tokens}}",
|
||||
"Invalidate": "Invalidate",
|
||||
"Invalidated": "Invalidated",
|
||||
"Invert match": "Invert match",
|
||||
@@ -2650,6 +2654,7 @@
|
||||
"Maximum check-in quota": "Maximum check-in quota",
|
||||
"Maximum custom groups per token": "Maximum custom groups per token",
|
||||
"Maximum input window": "Maximum input window",
|
||||
"Maximum number of channels tested at the same time (1-32)": "Maximum number of channels tested at the same time (1-32)",
|
||||
"Maximum number of tokens each user can create. Default 1000. Setting too large may affect performance.": "Maximum number of tokens each user can create. Default 1000. Setting too large may affect performance.",
|
||||
"Maximum number of tokens in the response": "Maximum number of tokens in the response",
|
||||
"Maximum quota amount awarded for check-in": "Maximum quota amount awarded for check-in",
|
||||
|
||||
@@ -776,6 +776,8 @@
|
||||
"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 concurrency": "Parallélisme des tests de canaux",
|
||||
"Channel test concurrency must be between 1 and 32": "Le parallélisme des tests de canaux doit être compris entre 1 et 32",
|
||||
"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",
|
||||
@@ -2377,6 +2379,7 @@
|
||||
"Internal Notes": "Notes internes",
|
||||
"Internal notes (not shown to users)": "Notes internes (non visibles par les utilisateurs)",
|
||||
"Internal Server Error!": "Erreur interne du serveur !",
|
||||
"Interval must be at least 1 minute": "L'intervalle doit être d'au moins 1 minute",
|
||||
"Invalid (NaN)": "Invalide (NaN)",
|
||||
"Invalid chat link. Please contact the administrator.": "Lien de chat invalide. Veuillez contacter l'administrateur.",
|
||||
"Invalid chat link. Please contact your administrator.": "Lien de chat invalide. Veuillez contacter votre administrateur.",
|
||||
@@ -2394,6 +2397,7 @@
|
||||
"Invalid reset link, please request a new password reset.": "Lien de réinitialisation invalide, veuillez demander une nouvelle réinitialisation du mot de passe.",
|
||||
"Invalid rules JSON format": "Format JSON des règles invalide",
|
||||
"Invalid status code mapping entries: {{entries}}": "Entrées de mappage de code d'état invalides : {{entries}}",
|
||||
"Invalid status code rules: {{tokens}}": "Règles de code de statut invalides : {{tokens}}",
|
||||
"Invalidate": "Invalider",
|
||||
"Invalidated": "Invalidé",
|
||||
"Invert match": "Inverser la correspondance",
|
||||
@@ -2650,6 +2654,7 @@
|
||||
"Maximum check-in quota": "Quota maximum de connexion",
|
||||
"Maximum custom groups per token": "Nombre maximal de groupes personnalisés par jeton",
|
||||
"Maximum input window": "Fenêtre d'entrée maximale",
|
||||
"Maximum number of channels tested at the same time (1-32)": "Nombre maximal de canaux testés simultanément (1 à 32)",
|
||||
"Maximum number of tokens each user can create. Default 1000. Setting too large may affect performance.": "Nombre maximum de jetons que chaque utilisateur peut créer. Par défaut 1000. Une valeur trop élevée peut affecter les performances.",
|
||||
"Maximum number of tokens in the response": "Nombre maximum de jetons dans la réponse",
|
||||
"Maximum quota amount awarded for check-in": "Montant maximum de quota attribué pour la connexion",
|
||||
|
||||
@@ -776,6 +776,8 @@
|
||||
"Channel models": "チャネルモデル",
|
||||
"Channel name is required": "チャネル名が必要です",
|
||||
"Channel test completed": "チャネルテストが完了しました",
|
||||
"Channel test concurrency": "チャンネルテストの同時実行数",
|
||||
"Channel test concurrency must be between 1 and 32": "チャンネルテストの同時実行数は1~32にしてください",
|
||||
"Channel test mode": "チャネルテストモード",
|
||||
"Channel type is required": "チャネルタイプが必要です",
|
||||
"Channel updated successfully": "チャネルが正常に更新されました",
|
||||
@@ -2377,6 +2379,7 @@
|
||||
"Internal Notes": "内部メモ",
|
||||
"Internal notes (not shown to users)": ":内部メモ(ユーザーには表示されません)",
|
||||
"Internal Server Error!": "内部サーバーエラー!",
|
||||
"Interval must be at least 1 minute": "間隔は1分以上にしてください",
|
||||
"Invalid (NaN)": "無効 (NaN)",
|
||||
"Invalid chat link. Please contact the administrator.": "無効なチャットリンクです。管理者に連絡してください。",
|
||||
"Invalid chat link. Please contact your administrator.": "無効なチャットリンクです。管理者に連絡してください。",
|
||||
@@ -2394,6 +2397,7 @@
|
||||
"Invalid reset link, please request a new password reset.": "無効なリセットリンクです。新しいパスワードリセットをリクエストしてください。",
|
||||
"Invalid rules JSON format": "ルール JSON の形式が不正です",
|
||||
"Invalid status code mapping entries: {{entries}}": "無効なステータスコードマッピング:{{entries}}",
|
||||
"Invalid status code rules: {{tokens}}": "無効なステータスコードルール:{{tokens}}",
|
||||
"Invalidate": "無効化",
|
||||
"Invalidated": "無効化済み",
|
||||
"Invert match": "一致を反転",
|
||||
@@ -2650,6 +2654,7 @@
|
||||
"Maximum check-in quota": "最大チェックインクォータ",
|
||||
"Maximum custom groups per token": "トークンごとのカスタムグループ上限",
|
||||
"Maximum input window": "最大入力ウィンドウ",
|
||||
"Maximum number of channels tested at the same time (1-32)": "同時にテストするチャンネルの最大数(1~32)",
|
||||
"Maximum number of tokens each user can create. Default 1000. Setting too large may affect performance.": "各ユーザーが作成できる最大トークン数。デフォルトは 1000。大きすぎる値はパフォーマンスに影響を与える可能性があります。",
|
||||
"Maximum number of tokens in the response": "レスポンスの最大トークン数",
|
||||
"Maximum quota amount awarded for check-in": "チェックインで付与される最大クォータ量",
|
||||
|
||||
@@ -776,6 +776,8 @@
|
||||
"Channel models": "Модели каналов",
|
||||
"Channel name is required": "Имя канала обязательно",
|
||||
"Channel test completed": "Тест канала завершён",
|
||||
"Channel test concurrency": "Параллельность проверки каналов",
|
||||
"Channel test concurrency must be between 1 and 32": "Параллельность проверки каналов должна быть от 1 до 32",
|
||||
"Channel test mode": "Режим проверки каналов",
|
||||
"Channel type is required": "Тип канала обязателен",
|
||||
"Channel updated successfully": "Канал успешно обновлён",
|
||||
@@ -2377,6 +2379,7 @@
|
||||
"Internal Notes": "Внутренние заметки",
|
||||
"Internal notes (not shown to users)": "Внутренние заметки (не показываются пользователям)",
|
||||
"Internal Server Error!": "Внутренняя ошибка сервера!",
|
||||
"Interval must be at least 1 minute": "Интервал должен быть не менее 1 минуты",
|
||||
"Invalid (NaN)": "Недопустимо (NaN)",
|
||||
"Invalid chat link. Please contact the administrator.": "Неверная ссылка на чат. Пожалуйста, обратитесь к администратору.",
|
||||
"Invalid chat link. Please contact your administrator.": "Недействительная ссылка чата. Обратитесь к администратору.",
|
||||
@@ -2394,6 +2397,7 @@
|
||||
"Invalid reset link, please request a new password reset.": "Недействительная ссылка для сброса, пожалуйста, запросите новый сброс пароля.",
|
||||
"Invalid rules JSON format": "Неверный формат JSON правил",
|
||||
"Invalid status code mapping entries: {{entries}}": "Недопустимые записи маппинга кодов состояния: {{entries}}",
|
||||
"Invalid status code rules: {{tokens}}": "Недопустимые правила кодов состояния: {{tokens}}",
|
||||
"Invalidate": "Аннулировать",
|
||||
"Invalidated": "Аннулирована",
|
||||
"Invert match": "Инвертировать совпадение",
|
||||
@@ -2650,6 +2654,7 @@
|
||||
"Maximum check-in quota": "Максимальная квота регистрации",
|
||||
"Maximum custom groups per token": "Максимум пользовательских групп на токен",
|
||||
"Maximum input window": "Максимальное окно ввода",
|
||||
"Maximum number of channels tested at the same time (1-32)": "Максимальное число одновременно проверяемых каналов (1–32)",
|
||||
"Maximum number of tokens each user can create. Default 1000. Setting too large may affect performance.": "Максимальное количество токенов, которое может создать каждый пользователь. По умолчанию 1000. Слишком большое значение может повлиять на производительность.",
|
||||
"Maximum number of tokens in the response": "Максимальное число токенов в ответе",
|
||||
"Maximum quota amount awarded for check-in": "Максимальная сумма квоты, присуждаемая за регистрацию",
|
||||
|
||||
@@ -776,6 +776,8 @@
|
||||
"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 concurrency": "Mức đồng thời khi kiểm tra kênh",
|
||||
"Channel test concurrency must be between 1 and 32": "Mức đồng thời khi kiểm tra kênh phải từ 1 đến 32",
|
||||
"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",
|
||||
@@ -2377,6 +2379,7 @@
|
||||
"Internal Notes": "Ghi chú nội bộ",
|
||||
"Internal notes (not shown to users)": "Ghi chú nội bộ (không hiển thị cho người dùng)",
|
||||
"Internal Server Error!": "Lỗi máy chủ nội bộ!",
|
||||
"Interval must be at least 1 minute": "Khoảng thời gian phải ít nhất 1 phút",
|
||||
"Invalid (NaN)": "Không hợp lệ (NaN)",
|
||||
"Invalid chat link. Please contact the administrator.": "Liên kết trò chuyện không hợp lệ. Vui lòng liên hệ quản trị viên.",
|
||||
"Invalid chat link. Please contact your administrator.": "Liên kết trò chuyện không hợp lệ. Vui lòng liên hệ với quản trị viên của bạn.",
|
||||
@@ -2394,6 +2397,7 @@
|
||||
"Invalid reset link, please request a new password reset.": "Liên kết đặt lại không hợp lệ, vui lòng yêu cầu đặt lại mật khẩu mới.",
|
||||
"Invalid rules JSON format": "Định dạng JSON quy tắc không hợp lệ",
|
||||
"Invalid status code mapping entries: {{entries}}": "Mục ánh xạ mã trạng thái không hợp lệ: {{entries}}",
|
||||
"Invalid status code rules: {{tokens}}": "Quy tắc mã trạng thái không hợp lệ: {{tokens}}",
|
||||
"Invalidate": "Vô hiệu hóa",
|
||||
"Invalidated": "Đã vô hiệu",
|
||||
"Invert match": "Đảo điều kiện khớp",
|
||||
@@ -2650,6 +2654,7 @@
|
||||
"Maximum check-in quota": "Hạn ngạch điểm danh tối đa",
|
||||
"Maximum custom groups per token": "Số nhóm tùy chỉnh tối đa cho mỗi token",
|
||||
"Maximum input window": "Cửa sổ nhập tối đa",
|
||||
"Maximum number of channels tested at the same time (1-32)": "Số kênh tối đa được kiểm tra cùng lúc (1–32)",
|
||||
"Maximum number of tokens each user can create. Default 1000. Setting too large may affect performance.": "Số lượng token tối đa mỗi người dùng có thể tạo. Mặc định là 1000. Đặt quá lớn có thể ảnh hưởng đến hiệu suất.",
|
||||
"Maximum number of tokens in the response": "Số token tối đa trong phản hồi",
|
||||
"Maximum quota amount awarded for check-in": "Số lượng hạn ngạch tối đa được trao cho điểm danh",
|
||||
|
||||
@@ -776,6 +776,8 @@
|
||||
"Channel models": "渠道模型",
|
||||
"Channel name is required": "渠道名稱是必填的",
|
||||
"Channel test completed": "渠道測試完成",
|
||||
"Channel test concurrency": "渠道測試並行數",
|
||||
"Channel test concurrency must be between 1 and 32": "渠道測試並行數必須介於 1 到 32 之間",
|
||||
"Channel test mode": "渠道測試模式",
|
||||
"Channel type is required": "渠道類型是必填的",
|
||||
"Channel updated successfully": "渠道更新成功",
|
||||
@@ -2377,6 +2379,7 @@
|
||||
"Internal Notes": "內部備註",
|
||||
"Internal notes (not shown to users)": "內部備註(不顯示給用戶)",
|
||||
"Internal Server Error!": "內部伺服器錯誤!",
|
||||
"Interval must be at least 1 minute": "間隔必須至少為 1 分鐘",
|
||||
"Invalid (NaN)": "無效 (NaN)",
|
||||
"Invalid chat link. Please contact the administrator.": "無效的聊天連結。請聯絡管理員。",
|
||||
"Invalid chat link. Please contact your administrator.": "無效的聊天連結。請聯絡您的管理員。",
|
||||
@@ -2394,6 +2397,7 @@
|
||||
"Invalid reset link, please request a new password reset.": "無效的重置連結,請請求新的密碼重置。",
|
||||
"Invalid rules JSON format": "規則 JSON 格式不正確",
|
||||
"Invalid status code mapping entries: {{entries}}": "無效的狀態碼映射條目:{{entries}}",
|
||||
"Invalid status code rules: {{tokens}}": "無效的狀態碼規則:{{tokens}}",
|
||||
"Invalidate": "作廢",
|
||||
"Invalidated": "已作廢",
|
||||
"Invert match": "反向匹配",
|
||||
@@ -2650,6 +2654,7 @@
|
||||
"Maximum check-in quota": "簽到最大額度",
|
||||
"Maximum custom groups per token": "每個令牌的最大自訂分組數",
|
||||
"Maximum input window": "最大輸入窗口",
|
||||
"Maximum number of channels tested at the same time (1-32)": "同時測試的最大渠道數(1-32)",
|
||||
"Maximum number of tokens each user can create. Default 1000. Setting too large may affect performance.": "每個用戶可建立的最大令牌數量。預設 1000。設定過大可能會影響效能。",
|
||||
"Maximum number of tokens in the response": "回應中最大 token 數",
|
||||
"Maximum quota amount awarded for check-in": "簽到獎勵的最大額度",
|
||||
|
||||
@@ -776,6 +776,8 @@
|
||||
"Channel models": "渠道模型",
|
||||
"Channel name is required": "渠道名称是必填的",
|
||||
"Channel test completed": "渠道测试完成",
|
||||
"Channel test concurrency": "渠道测试并发数",
|
||||
"Channel test concurrency must be between 1 and 32": "渠道测试并发数必须在 1 到 32 之间",
|
||||
"Channel test mode": "渠道测试模式",
|
||||
"Channel type is required": "渠道类型是必填的",
|
||||
"Channel updated successfully": "渠道更新成功",
|
||||
@@ -2377,6 +2379,7 @@
|
||||
"Internal Notes": "内部备注",
|
||||
"Internal notes (not shown to users)": "内部备注(不显示给用户)",
|
||||
"Internal Server Error!": "内部服务器错误!",
|
||||
"Interval must be at least 1 minute": "间隔必须至少为 1 分钟",
|
||||
"Invalid (NaN)": "无效 (NaN)",
|
||||
"Invalid chat link. Please contact the administrator.": "无效的聊天链接。请联系管理员。",
|
||||
"Invalid chat link. Please contact your administrator.": "无效的聊天链接。请联系您的管理员。",
|
||||
@@ -2394,6 +2397,7 @@
|
||||
"Invalid reset link, please request a new password reset.": "无效的重置链接,请请求新的密码重置。",
|
||||
"Invalid rules JSON format": "规则 JSON 格式不正确",
|
||||
"Invalid status code mapping entries: {{entries}}": "无效的状态码映射条目:{{entries}}",
|
||||
"Invalid status code rules: {{tokens}}": "无效的状态码规则:{{tokens}}",
|
||||
"Invalidate": "作废",
|
||||
"Invalidated": "已作废",
|
||||
"Invert match": "反向匹配",
|
||||
@@ -2650,6 +2654,7 @@
|
||||
"Maximum check-in quota": "签到最大额度",
|
||||
"Maximum custom groups per token": "每个令牌的最大自定义分组数",
|
||||
"Maximum input window": "最大输入窗口",
|
||||
"Maximum number of channels tested at the same time (1-32)": "同时测试的最大渠道数(1-32)",
|
||||
"Maximum number of tokens each user can create. Default 1000. Setting too large may affect performance.": "每个用户可创建的最大令牌数量。默认 1000。设置过大可能会影响性能。",
|
||||
"Maximum number of tokens in the response": "响应中最大 token 数",
|
||||
"Maximum quota amount awarded for check-in": "签到奖励的最大额度",
|
||||
|
||||
Reference in New Issue
Block a user