mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-09-12 15:21:09 +00:00
+149
-72
@@ -12,6 +12,7 @@ import (
|
|||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/QuantumNous/new-api/common"
|
"github.com/QuantumNous/new-api/common"
|
||||||
@@ -908,92 +909,167 @@ type channelTestSummary struct {
|
|||||||
Enabled int `json:"enabled"`
|
Enabled int `json:"enabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// performChannelTests runs the channel test loop synchronously, honoring ctx
|
func testChannelForHealthCheck(ctx context.Context, channel *model.Channel, testUserID int, allowDisable bool, disableThreshold int64) channelTestSummary {
|
||||||
// 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 {
|
|
||||||
summary := channelTestSummary{}
|
summary := channelTestSummary{}
|
||||||
var disableThreshold = int64(common.ChannelDisableThreshold * 1000)
|
isChannelEnabled := channel.Status == common.ChannelStatusEnabled
|
||||||
if disableThreshold == 0 {
|
tik := time.Now()
|
||||||
disableThreshold = 10000000 // a impossible value
|
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)
|
total := len(channels)
|
||||||
for index, channel := range channels {
|
if report != nil {
|
||||||
if ctx != nil && ctx.Err() != nil {
|
report(0, total)
|
||||||
break
|
}
|
||||||
}
|
if total == 0 {
|
||||||
if report != nil {
|
return channelTestSummary{}
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
summary.Tested++
|
workerCount := min(operation_setting.NormalizeChannelTestConcurrency(concurrency), total)
|
||||||
|
jobs := make(chan *model.Channel)
|
||||||
|
results := make(chan channelTestSummary)
|
||||||
|
|
||||||
shouldBanChannel := false
|
var workers sync.WaitGroup
|
||||||
newAPIError := result.newAPIError
|
workers.Add(workerCount)
|
||||||
// request error disables the channel
|
for range workerCount {
|
||||||
if newAPIError != nil {
|
go func() {
|
||||||
shouldBanChannel = service.ShouldDisableChannel(result.newAPIError)
|
defer workers.Done()
|
||||||
}
|
for {
|
||||||
|
|
||||||
// 当错误检查通过,才检查响应时间
|
|
||||||
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 {
|
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return summary
|
return
|
||||||
case <-time.After(common.RequestInterval):
|
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
|
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
|
// runChannelTestTask runs one synchronous channel test cycle for the system task
|
||||||
// runner (both the scheduled job and the manual "test all channels" trigger go
|
// 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
|
// 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)
|
selected := selectChannelsForAutomaticTest(channels, mode)
|
||||||
allowDisable := mode != operation_setting.ChannelTestModePassiveRecovery
|
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) {
|
if notify && (ctx == nil || ctx.Err() == nil) {
|
||||||
service.NotifyRootUser(dto.NotifyTypeChannelTest, "通道测试完成", "所有通道测试已完成")
|
service.NotifyRootUser(dto.NotifyTypeChannelTest, "通道测试完成", "所有通道测试已完成")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ package controller
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/QuantumNous/new-api/common"
|
"github.com/QuantumNous/new-api/common"
|
||||||
@@ -339,6 +341,111 @@ func TestSelectChannelsForAutomaticTestAutoBanOnlyUsesEligibleChannels(t *testin
|
|||||||
require.Equal(t, 3, selected[1].Id)
|
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) {
|
func TestTestAllChannelsRejectsExistingActiveTask(t *testing.T) {
|
||||||
db := setupModelListControllerTestDB(t)
|
db := setupModelListControllerTestDB(t)
|
||||||
require.NoError(t, db.AutoMigrate(&model.SystemTask{}, &model.SystemTaskLock{}))
|
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 {
|
if key == operation_setting.ToolPriceOptionKey {
|
||||||
return operation_setting.ValidateToolPricesJSON(value)
|
return operation_setting.ValidateToolPricesJSON(value)
|
||||||
}
|
}
|
||||||
|
if key == operation_setting.ChannelTestConcurrencyOptionKey {
|
||||||
|
return operation_setting.ValidateChannelTestConcurrency(value)
|
||||||
|
}
|
||||||
if key == "MaxTokenAutoGroups" {
|
if key == "MaxTokenAutoGroups" {
|
||||||
return setting.ValidateMaxTokenAutoGroups(value)
|
return setting.ValidateMaxTokenAutoGroups(value)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package operation_setting
|
package operation_setting
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
@@ -11,12 +12,17 @@ type MonitorSetting struct {
|
|||||||
AutoTestChannelEnabled bool `json:"auto_test_channel_enabled"`
|
AutoTestChannelEnabled bool `json:"auto_test_channel_enabled"`
|
||||||
AutoTestChannelMinutes float64 `json:"auto_test_channel_minutes"`
|
AutoTestChannelMinutes float64 `json:"auto_test_channel_minutes"`
|
||||||
ChannelTestMode string `json:"channel_test_mode"`
|
ChannelTestMode string `json:"channel_test_mode"`
|
||||||
|
ChannelTestConcurrency int `json:"channel_test_concurrency"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ChannelTestModeScheduledAll = "scheduled_all"
|
ChannelTestModeScheduledAll = "scheduled_all"
|
||||||
ChannelTestModeAutoBanOnly = "auto_ban_only"
|
ChannelTestModeAutoBanOnly = "auto_ban_only"
|
||||||
ChannelTestModePassiveRecovery = "passive_recovery"
|
ChannelTestModePassiveRecovery = "passive_recovery"
|
||||||
|
|
||||||
|
ChannelTestConcurrencyOptionKey = "monitor_setting.channel_test_concurrency"
|
||||||
|
DefaultChannelTestConcurrency = 1
|
||||||
|
MaxChannelTestConcurrency = 32
|
||||||
)
|
)
|
||||||
|
|
||||||
// 默认配置
|
// 默认配置
|
||||||
@@ -24,6 +30,7 @@ var monitorSetting = MonitorSetting{
|
|||||||
AutoTestChannelEnabled: false,
|
AutoTestChannelEnabled: false,
|
||||||
AutoTestChannelMinutes: 10,
|
AutoTestChannelMinutes: 10,
|
||||||
ChannelTestMode: ChannelTestModeScheduledAll,
|
ChannelTestMode: ChannelTestModeScheduledAll,
|
||||||
|
ChannelTestConcurrency: DefaultChannelTestConcurrency,
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -51,5 +58,24 @@ func GetMonitorSetting() *MonitorSetting {
|
|||||||
default:
|
default:
|
||||||
monitorSetting.ChannelTestMode = ChannelTestModeScheduledAll
|
monitorSetting.ChannelTestMode = ChannelTestModeScheduledAll
|
||||||
}
|
}
|
||||||
|
monitorSetting.ChannelTestConcurrency = NormalizeChannelTestConcurrency(monitorSetting.ChannelTestConcurrency)
|
||||||
return &monitorSetting
|
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)
|
require.NotNil(t, setting)
|
||||||
assert.Equal(t, ChannelTestModeAutoBanOnly, setting.ChannelTestMode)
|
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',
|
'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_enabled': false,
|
||||||
'monitor_setting.auto_test_channel_minutes': 10,
|
'monitor_setting.auto_test_channel_minutes': 10,
|
||||||
|
'monitor_setting.channel_test_concurrency': 1,
|
||||||
'monitor_setting.channel_test_mode': 'scheduled_all',
|
'monitor_setting.channel_test_mode': 'scheduled_all',
|
||||||
'channel_affinity_setting.enabled': false,
|
'channel_affinity_setting.enabled': false,
|
||||||
'channel_affinity_setting.switch_on_success': true,
|
'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',
|
'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_enabled': false,
|
||||||
'monitor_setting.auto_test_channel_minutes': 10,
|
'monitor_setting.auto_test_channel_minutes': 10,
|
||||||
|
'monitor_setting.channel_test_concurrency': 1,
|
||||||
'monitor_setting.channel_test_mode': 'scheduled_all',
|
'monitor_setting.channel_test_mode': 'scheduled_all',
|
||||||
'channel_affinity_setting.enabled': false,
|
'channel_affinity_setting.enabled': false,
|
||||||
'channel_affinity_setting.switch_on_success': true,
|
'channel_affinity_setting.switch_on_success': true,
|
||||||
|
|||||||
@@ -69,55 +69,70 @@ const channelTestModes = [
|
|||||||
'passive_recovery',
|
'passive_recovery',
|
||||||
] as const
|
] as const
|
||||||
type ChannelTestMode = (typeof channelTestModes)[number]
|
type ChannelTestMode = (typeof channelTestModes)[number]
|
||||||
|
const MAX_CHANNEL_TEST_CONCURRENCY = 32
|
||||||
|
|
||||||
const routingReliabilitySchema = z
|
const createRoutingReliabilitySchema = (
|
||||||
.object({
|
t: (key: string, options?: Record<string, unknown>) => string
|
||||||
RetryTimes: z.coerce.number().min(0).max(10),
|
) =>
|
||||||
ChannelDisableThreshold: numericString,
|
z
|
||||||
AutomaticDisableChannelEnabled: z.boolean(),
|
.object({
|
||||||
AutomaticEnableChannelEnabled: z.boolean(),
|
RetryTimes: z.coerce.number().min(0).max(10),
|
||||||
AutomaticDisableKeywords: z.string(),
|
ChannelDisableThreshold: numericString,
|
||||||
AutomaticDisableStatusCodes: z.string(),
|
AutomaticDisableChannelEnabled: z.boolean(),
|
||||||
AutomaticRetryStatusCodes: z.string(),
|
AutomaticEnableChannelEnabled: z.boolean(),
|
||||||
monitor_setting: z.object({
|
AutomaticDisableKeywords: z.string(),
|
||||||
auto_test_channel_enabled: z.boolean(),
|
AutomaticDisableStatusCodes: z.string(),
|
||||||
auto_test_channel_minutes: z.coerce
|
AutomaticRetryStatusCodes: z.string(),
|
||||||
.number()
|
monitor_setting: z.object({
|
||||||
.int()
|
auto_test_channel_enabled: z.boolean(),
|
||||||
.min(1, 'Interval must be at least 1 minute'),
|
auto_test_channel_minutes: z.coerce
|
||||||
channel_test_mode: z.enum(channelTestModes),
|
.number()
|
||||||
}),
|
.int()
|
||||||
})
|
.min(1, t('Interval must be at least 1 minute')),
|
||||||
.superRefine((values, ctx) => {
|
channel_test_concurrency: z.coerce
|
||||||
const disableParsed = parseHttpStatusCodeRules(
|
.number()
|
||||||
values.AutomaticDisableStatusCodes
|
.int(t('Enter a positive integer'))
|
||||||
)
|
.min(1, t('Channel test concurrency must be between 1 and 32'))
|
||||||
if (!disableParsed.ok) {
|
.max(
|
||||||
ctx.addIssue({
|
MAX_CHANNEL_TEST_CONCURRENCY,
|
||||||
code: 'custom',
|
t('Channel test concurrency must be between 1 and 32')
|
||||||
path: ['AutomaticDisableStatusCodes'],
|
),
|
||||||
message: `Invalid status code rules: ${disableParsed.invalidTokens.join(
|
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(
|
const retryParsed = parseHttpStatusCodeRules(
|
||||||
values.AutomaticRetryStatusCodes
|
values.AutomaticRetryStatusCodes
|
||||||
)
|
)
|
||||||
if (!retryParsed.ok) {
|
if (!retryParsed.ok) {
|
||||||
ctx.addIssue({
|
ctx.addIssue({
|
||||||
code: 'custom',
|
code: 'custom',
|
||||||
path: ['AutomaticRetryStatusCodes'],
|
path: ['AutomaticRetryStatusCodes'],
|
||||||
message: `Invalid status code rules: ${retryParsed.invalidTokens.join(
|
message: t('Invalid status code rules: {{tokens}}', {
|
||||||
', '
|
tokens: retryParsed.invalidTokens.join(', '),
|
||||||
)}`,
|
}),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
type RoutingReliabilityFormValues = z.output<typeof routingReliabilitySchema>
|
type RoutingReliabilitySchema = ReturnType<
|
||||||
type RoutingReliabilityFormInput = z.input<typeof routingReliabilitySchema>
|
typeof createRoutingReliabilitySchema
|
||||||
|
>
|
||||||
|
type RoutingReliabilityFormValues = z.output<RoutingReliabilitySchema>
|
||||||
|
type RoutingReliabilityFormInput = z.input<RoutingReliabilitySchema>
|
||||||
|
|
||||||
type RoutingReliabilitySectionProps = {
|
type RoutingReliabilitySectionProps = {
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -130,6 +145,7 @@ type RoutingReliabilitySectionProps = {
|
|||||||
AutomaticRetryStatusCodes: string
|
AutomaticRetryStatusCodes: string
|
||||||
'monitor_setting.auto_test_channel_enabled': boolean
|
'monitor_setting.auto_test_channel_enabled': boolean
|
||||||
'monitor_setting.auto_test_channel_minutes': number
|
'monitor_setting.auto_test_channel_minutes': number
|
||||||
|
'monitor_setting.channel_test_concurrency': number
|
||||||
'monitor_setting.channel_test_mode': ChannelTestMode
|
'monitor_setting.channel_test_mode': ChannelTestMode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -148,6 +164,7 @@ type NormalizedRoutingReliabilityValues = {
|
|||||||
AutomaticRetryStatusCodes: string
|
AutomaticRetryStatusCodes: string
|
||||||
'monitor_setting.auto_test_channel_enabled': boolean
|
'monitor_setting.auto_test_channel_enabled': boolean
|
||||||
'monitor_setting.auto_test_channel_minutes': number
|
'monitor_setting.auto_test_channel_minutes': number
|
||||||
|
'monitor_setting.channel_test_concurrency': number
|
||||||
'monitor_setting.channel_test_mode': ChannelTestMode
|
'monitor_setting.channel_test_mode': ChannelTestMode
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,6 +192,8 @@ const buildFormDefaults = (
|
|||||||
defaults['monitor_setting.auto_test_channel_enabled'],
|
defaults['monitor_setting.auto_test_channel_enabled'],
|
||||||
auto_test_channel_minutes:
|
auto_test_channel_minutes:
|
||||||
defaults['monitor_setting.auto_test_channel_minutes'],
|
defaults['monitor_setting.auto_test_channel_minutes'],
|
||||||
|
channel_test_concurrency:
|
||||||
|
defaults['monitor_setting.channel_test_concurrency'],
|
||||||
channel_test_mode: normalizeChannelTestMode(
|
channel_test_mode: normalizeChannelTestMode(
|
||||||
defaults['monitor_setting.channel_test_mode']
|
defaults['monitor_setting.channel_test_mode']
|
||||||
),
|
),
|
||||||
@@ -201,6 +220,8 @@ const normalizeDefaults = (
|
|||||||
defaults['monitor_setting.auto_test_channel_enabled'],
|
defaults['monitor_setting.auto_test_channel_enabled'],
|
||||||
'monitor_setting.auto_test_channel_minutes':
|
'monitor_setting.auto_test_channel_minutes':
|
||||||
defaults['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(
|
'monitor_setting.channel_test_mode': normalizeChannelTestMode(
|
||||||
defaults['monitor_setting.channel_test_mode']
|
defaults['monitor_setting.channel_test_mode']
|
||||||
),
|
),
|
||||||
@@ -226,6 +247,8 @@ const normalizeFormValues = (
|
|||||||
values.monitor_setting.auto_test_channel_enabled,
|
values.monitor_setting.auto_test_channel_enabled,
|
||||||
'monitor_setting.auto_test_channel_minutes':
|
'monitor_setting.auto_test_channel_minutes':
|
||||||
values.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,
|
'monitor_setting.channel_test_mode': values.monitor_setting.channel_test_mode,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -234,6 +257,7 @@ export function RoutingReliabilitySection({
|
|||||||
}: RoutingReliabilitySectionProps) {
|
}: RoutingReliabilitySectionProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const updateOption = useUpdateOption()
|
const updateOption = useUpdateOption()
|
||||||
|
const routingReliabilitySchema = createRoutingReliabilitySchema(t)
|
||||||
const baselineRef = useRef<NormalizedRoutingReliabilityValues>(
|
const baselineRef = useRef<NormalizedRoutingReliabilityValues>(
|
||||||
normalizeDefaults(defaultValues)
|
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
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name='AutomaticEnableChannelEnabled'
|
name='AutomaticEnableChannelEnabled'
|
||||||
|
|||||||
@@ -83,6 +83,8 @@ const MODELS_SECTIONS = [
|
|||||||
settings['monitor_setting.auto_test_channel_enabled'],
|
settings['monitor_setting.auto_test_channel_enabled'],
|
||||||
'monitor_setting.auto_test_channel_minutes':
|
'monitor_setting.auto_test_channel_minutes':
|
||||||
settings['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':
|
'monitor_setting.channel_test_mode':
|
||||||
settings['monitor_setting.channel_test_mode'],
|
settings['monitor_setting.channel_test_mode'],
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -235,6 +235,7 @@ export type ModelSettings = {
|
|||||||
AutomaticRetryStatusCodes: string
|
AutomaticRetryStatusCodes: string
|
||||||
'monitor_setting.auto_test_channel_enabled': boolean
|
'monitor_setting.auto_test_channel_enabled': boolean
|
||||||
'monitor_setting.auto_test_channel_minutes': number
|
'monitor_setting.auto_test_channel_minutes': number
|
||||||
|
'monitor_setting.channel_test_concurrency': number
|
||||||
'monitor_setting.channel_test_mode':
|
'monitor_setting.channel_test_mode':
|
||||||
| 'scheduled_all'
|
| 'scheduled_all'
|
||||||
| 'auto_ban_only'
|
| 'auto_ban_only'
|
||||||
|
|||||||
@@ -776,6 +776,8 @@
|
|||||||
"Channel models": "Channel models",
|
"Channel models": "Channel models",
|
||||||
"Channel name is required": "Channel name is required",
|
"Channel name is required": "Channel name is required",
|
||||||
"Channel test completed": "Channel test completed",
|
"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 test mode": "Channel test mode",
|
||||||
"Channel type is required": "Channel type is required",
|
"Channel type is required": "Channel type is required",
|
||||||
"Channel updated successfully": "Channel updated successfully",
|
"Channel updated successfully": "Channel updated successfully",
|
||||||
@@ -2377,6 +2379,7 @@
|
|||||||
"Internal Notes": "Internal Notes",
|
"Internal Notes": "Internal Notes",
|
||||||
"Internal notes (not shown to users)": "Internal notes (not shown to users)",
|
"Internal notes (not shown to users)": "Internal notes (not shown to users)",
|
||||||
"Internal Server Error!": "Internal Server Error!",
|
"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 (NaN)": "Invalid (NaN)",
|
||||||
"Invalid chat link. Please contact the administrator.": "Invalid chat link. Please contact the administrator.",
|
"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.",
|
"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 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 rules JSON format": "Invalid rules JSON format",
|
||||||
"Invalid status code mapping entries: {{entries}}": "Invalid status code mapping entries: {{entries}}",
|
"Invalid status code mapping entries: {{entries}}": "Invalid status code mapping entries: {{entries}}",
|
||||||
|
"Invalid status code rules: {{tokens}}": "Invalid status code rules: {{tokens}}",
|
||||||
"Invalidate": "Invalidate",
|
"Invalidate": "Invalidate",
|
||||||
"Invalidated": "Invalidated",
|
"Invalidated": "Invalidated",
|
||||||
"Invert match": "Invert match",
|
"Invert match": "Invert match",
|
||||||
@@ -2650,6 +2654,7 @@
|
|||||||
"Maximum check-in quota": "Maximum check-in quota",
|
"Maximum check-in quota": "Maximum check-in quota",
|
||||||
"Maximum custom groups per token": "Maximum custom groups per token",
|
"Maximum custom groups per token": "Maximum custom groups per token",
|
||||||
"Maximum input window": "Maximum input window",
|
"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 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 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",
|
"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 models": "Modèles de canaux",
|
||||||
"Channel name is required": "Le nom du canal est requis",
|
"Channel name is required": "Le nom du canal est requis",
|
||||||
"Channel test completed": "Test du canal terminé",
|
"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 test mode": "Mode de test des canaux",
|
||||||
"Channel type is required": "Le type de canal est requis",
|
"Channel type is required": "Le type de canal est requis",
|
||||||
"Channel updated successfully": "Canal mis à jour avec succès",
|
"Channel updated successfully": "Canal mis à jour avec succès",
|
||||||
@@ -2377,6 +2379,7 @@
|
|||||||
"Internal Notes": "Notes internes",
|
"Internal Notes": "Notes internes",
|
||||||
"Internal notes (not shown to users)": "Notes internes (non visibles par les utilisateurs)",
|
"Internal notes (not shown to users)": "Notes internes (non visibles par les utilisateurs)",
|
||||||
"Internal Server Error!": "Erreur interne du serveur !",
|
"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 (NaN)": "Invalide (NaN)",
|
||||||
"Invalid chat link. Please contact the administrator.": "Lien de chat invalide. Veuillez contacter l'administrateur.",
|
"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.",
|
"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 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 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 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",
|
"Invalidate": "Invalider",
|
||||||
"Invalidated": "Invalidé",
|
"Invalidated": "Invalidé",
|
||||||
"Invert match": "Inverser la correspondance",
|
"Invert match": "Inverser la correspondance",
|
||||||
@@ -2650,6 +2654,7 @@
|
|||||||
"Maximum check-in quota": "Quota maximum de connexion",
|
"Maximum check-in quota": "Quota maximum de connexion",
|
||||||
"Maximum custom groups per token": "Nombre maximal de groupes personnalisés par jeton",
|
"Maximum custom groups per token": "Nombre maximal de groupes personnalisés par jeton",
|
||||||
"Maximum input window": "Fenêtre d'entrée maximale",
|
"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 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 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",
|
"Maximum quota amount awarded for check-in": "Montant maximum de quota attribué pour la connexion",
|
||||||
|
|||||||
@@ -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 must be between 1 and 32": "チャンネルテストの同時実行数は1~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": "間隔は1分以上にしてください",
|
||||||
"Invalid (NaN)": "無効 (NaN)",
|
"Invalid (NaN)": "無効 (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": "ルール JSON の形式が不正です",
|
"Invalid rules JSON format": "ルール JSON の形式が不正です",
|
||||||
"Invalid status code mapping entries: {{entries}}": "無効なステータスコードマッピング:{{entries}}",
|
"Invalid status code mapping entries: {{entries}}": "無効なステータスコードマッピング:{{entries}}",
|
||||||
|
"Invalid status code rules: {{tokens}}": "無効なステータスコードルール:{{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)": "同時にテストするチャンネルの最大数(1~32)",
|
||||||
"Maximum number of tokens each user can create. Default 1000. Setting too large may affect performance.": "各ユーザーが作成できる最大トークン数。デフォルトは 1000。大きすぎる値はパフォーマンスに影響を与える可能性があります。",
|
"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 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": "Модели каналов",
|
"Channel models": "Модели каналов",
|
||||||
"Channel name is required": "Имя канала обязательно",
|
"Channel name is required": "Имя канала обязательно",
|
||||||
"Channel test completed": "Тест канала завершён",
|
"Channel test completed": "Тест канала завершён",
|
||||||
|
"Channel test concurrency": "Параллельность проверки каналов",
|
||||||
|
"Channel test concurrency must be between 1 and 32": "Параллельность проверки каналов должна быть от 1 до 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": "Интервал должен быть не менее 1 минуты",
|
||||||
"Invalid (NaN)": "Недопустимо (NaN)",
|
"Invalid (NaN)": "Недопустимо (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": "Неверный формат JSON правил",
|
"Invalid rules JSON format": "Неверный формат JSON правил",
|
||||||
"Invalid status code mapping entries: {{entries}}": "Недопустимые записи маппинга кодов состояния: {{entries}}",
|
"Invalid status code mapping entries: {{entries}}": "Недопустимые записи маппинга кодов состояния: {{entries}}",
|
||||||
|
"Invalid status code rules: {{tokens}}": "Недопустимые правила кодов состояния: {{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)": "Максимальное число одновременно проверяемых каналов (1–32)",
|
||||||
"Maximum number of tokens each user can create. Default 1000. Setting too large may affect performance.": "Максимальное количество токенов, которое может создать каждый пользователь. По умолчанию 1000. Слишком большое значение может повлиять на производительность.",
|
"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 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": "Mô hình kênh",
|
"Channel models": "Mô hình kênh",
|
||||||
"Channel name is required": "Tên kênh là bắt buộc",
|
"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 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 test mode": "Chế độ kiểm tra kênh",
|
||||||
"Channel type is required": "Loại kênh là bắt buộc",
|
"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 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": "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 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ộ!",
|
"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 (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 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.",
|
"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 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 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 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",
|
"Invalidate": "Vô hiệu hóa",
|
||||||
"Invalidated": "Đã vô hiệu",
|
"Invalidated": "Đã vô hiệu",
|
||||||
"Invert match": "Đảo điều kiện khớp",
|
"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 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 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 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 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 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",
|
"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 models": "渠道模型",
|
||||||
"Channel name is required": "渠道名稱是必填的",
|
"Channel name is required": "渠道名稱是必填的",
|
||||||
"Channel test completed": "渠道測試完成",
|
"Channel test completed": "渠道測試完成",
|
||||||
|
"Channel test concurrency": "渠道測試並行數",
|
||||||
|
"Channel test concurrency must be between 1 and 32": "渠道測試並行數必須介於 1 到 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": "間隔必須至少為 1 分鐘",
|
||||||
"Invalid (NaN)": "無效 (NaN)",
|
"Invalid (NaN)": "無效 (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": "規則 JSON 格式不正確",
|
"Invalid rules JSON format": "規則 JSON 格式不正確",
|
||||||
"Invalid status code mapping entries: {{entries}}": "無效的狀態碼映射條目:{{entries}}",
|
"Invalid status code mapping entries: {{entries}}": "無效的狀態碼映射條目:{{entries}}",
|
||||||
|
"Invalid status code rules: {{tokens}}": "無效的狀態碼規則:{{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)": "同時測試的最大渠道數(1-32)",
|
||||||
"Maximum number of tokens each user can create. Default 1000. Setting too large may affect performance.": "每個用戶可建立的最大令牌數量。預設 1000。設定過大可能會影響效能。",
|
"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 number of tokens in the response": "回應中最大 token 數",
|
||||||
"Maximum quota amount awarded for check-in": "簽到獎勵的最大額度",
|
"Maximum quota amount awarded for check-in": "簽到獎勵的最大額度",
|
||||||
|
|||||||
@@ -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 must be between 1 and 32": "渠道测试并发数必须在 1 到 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": "间隔必须至少为 1 分钟",
|
||||||
"Invalid (NaN)": "无效 (NaN)",
|
"Invalid (NaN)": "无效 (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": "规则 JSON 格式不正确",
|
"Invalid rules JSON format": "规则 JSON 格式不正确",
|
||||||
"Invalid status code mapping entries: {{entries}}": "无效的状态码映射条目:{{entries}}",
|
"Invalid status code mapping entries: {{entries}}": "无效的状态码映射条目:{{entries}}",
|
||||||
|
"Invalid status code rules: {{tokens}}": "无效的状态码规则:{{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)": "同时测试的最大渠道数(1-32)",
|
||||||
"Maximum number of tokens each user can create. Default 1000. Setting too large may affect performance.": "每个用户可创建的最大令牌数量。默认 1000。设置过大可能会影响性能。",
|
"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 number of tokens in the response": "响应中最大 token 数",
|
||||||
"Maximum quota amount awarded for check-in": "签到奖励的最大额度",
|
"Maximum quota amount awarded for check-in": "签到奖励的最大额度",
|
||||||
|
|||||||
Reference in New Issue
Block a user