diff --git a/dto/channel_settings.go b/dto/channel_settings.go index 390853c9..8d460c79 100644 --- a/dto/channel_settings.go +++ b/dto/channel_settings.go @@ -33,13 +33,14 @@ type ChannelOtherSettings struct { AzureResponsesVersion string `json:"azure_responses_version,omitempty"` VertexKeyType VertexKeyType `json:"vertex_key_type,omitempty"` // "json" or "api_key" OpenRouterEnterprise *bool `json:"openrouter_enterprise,omitempty"` - ClaudeBetaQuery bool `json:"claude_beta_query,omitempty"` // Claude 渠道是否强制追加 ?beta=true - AllowServiceTier bool `json:"allow_service_tier,omitempty"` // 是否允许 service_tier 透传(默认过滤以避免额外计费) - AllowInferenceGeo bool `json:"allow_inference_geo,omitempty"` // 是否允许 inference_geo 透传(仅 Claude,默认过滤以满足数据驻留合规 - AllowSpeed bool `json:"allow_speed,omitempty"` // 是否允许 speed 透传(仅 Claude,默认过滤以避免意外切换推理速度模式) - AllowSafetyIdentifier bool `json:"allow_safety_identifier,omitempty"` // 是否允许 safety_identifier 透传(默认过滤以保护用户隐私) - DisableStore bool `json:"disable_store,omitempty"` // 是否禁用 store 透传(默认允许透传,禁用后可能导致 Codex 无法使用) - AllowIncludeObfuscation bool `json:"allow_include_obfuscation,omitempty"` // 是否允许 stream_options.include_obfuscation 透传(默认过滤以避免关闭流混淆保护) + ClaudeBetaQuery bool `json:"claude_beta_query,omitempty"` // Claude 渠道是否强制追加 ?beta=true + AllowServiceTier bool `json:"allow_service_tier,omitempty"` // 是否允许 service_tier 透传(默认过滤以避免额外计费) + AllowInferenceGeo bool `json:"allow_inference_geo,omitempty"` // 是否允许 inference_geo 透传(仅 Claude,默认过滤以满足数据驻留合规 + AllowSpeed bool `json:"allow_speed,omitempty"` // 是否允许 speed 透传(仅 Claude,默认过滤以避免意外切换推理速度模式) + AllowSafetyIdentifier bool `json:"allow_safety_identifier,omitempty"` // 是否允许 safety_identifier 透传(默认过滤以保护用户隐私) + DisableStore bool `json:"disable_store,omitempty"` // 是否禁用 store 透传(默认允许透传,禁用后可能导致 Codex 无法使用) + AllowIncludeObfuscation bool `json:"allow_include_obfuscation,omitempty"` // 是否允许 stream_options.include_obfuscation 透传(默认过滤以避免关闭流混淆保护) + DisableTaskPollingSleep bool `json:"disable_task_polling_sleep,omitempty"` // 是否跳过异步任务轮询间隔 AwsKeyType AwsKeyType `json:"aws_key_type,omitempty"` UpstreamModelUpdateCheckEnabled bool `json:"upstream_model_update_check_enabled,omitempty"` // 是否检测上游模型更新 UpstreamModelUpdateAutoSyncEnabled bool `json:"upstream_model_update_auto_sync_enabled,omitempty"` // 是否自动同步上游模型更新 diff --git a/service/task_polling.go b/service/task_polling.go index 4a6f90fd..179fa734 100644 --- a/service/task_polling.go +++ b/service/task_polling.go @@ -8,6 +8,7 @@ import ( "net/http" "sort" "strings" + "sync" "time" "github.com/QuantumNous/new-api/common" @@ -18,6 +19,7 @@ import ( "github.com/QuantumNous/new-api/relay/channel/task/taskcommon" relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/bytedance/gopkg/util/gopool" "github.com/samber/lo" ) @@ -338,19 +340,40 @@ func taskNeedsUpdate(oldTask *model.Task, newTask dto.SunoDataResponse) bool { // UpdateVideoTasks 按渠道更新所有视频任务 func UpdateVideoTasks(ctx context.Context, platform constant.TaskPlatform, taskChannelM map[int][]string, taskM map[string]*model.Task) error { - for channelId, taskIds := range taskChannelM { - if ctx.Err() != nil { - return ctx.Err() - } - if err := updateVideoTasks(ctx, platform, channelId, taskIds, taskM); err != nil { - logger.LogError(ctx, fmt.Sprintf("Channel #%d failed to update video async tasks: %s", channelId, err.Error())) + channelIDs := make([]int, 0, len(taskChannelM)) + for channelID := range taskChannelM { + channelIDs = append(channelIDs, channelID) + } + sort.Ints(channelIDs) + + var wg sync.WaitGroup + for _, channelId := range channelIDs { + taskIds := taskChannelM[channelId] + if len(taskIds) == 0 { + continue } + taskIds = append([]string(nil), taskIds...) + + wg.Add(1) + gopool.Go(func() { + defer wg.Done() + if err := updateVideoTasks(ctx, platform, channelId, taskIds, taskM); err != nil { + logger.LogError(ctx, fmt.Sprintf("Channel #%d failed to update video async tasks: %s", channelId, err.Error())) + } + }) + } + wg.Wait() + if ctx.Err() != nil { + return ctx.Err() } return nil } func updateVideoTasks(ctx context.Context, platform constant.TaskPlatform, channelId int, taskIds []string, taskM map[string]*model.Task) error { logger.LogInfo(ctx, fmt.Sprintf("Channel #%d pending video tasks: %d", channelId, len(taskIds))) + if ctx.Err() != nil { + return ctx.Err() + } if len(taskIds) == 0 { return nil } @@ -383,14 +406,19 @@ func updateVideoTasks(ctx context.Context, platform constant.TaskPlatform, chann } info.ApiKey = cacheGetChannel.Key adaptor.Init(info) - for _, taskId := range taskIds { + disablePollingSleep := cacheGetChannel.GetOtherSettings().DisableTaskPollingSleep + for i, taskId := range taskIds { if ctx.Err() != nil { return ctx.Err() } if err := updateVideoSingleTask(ctx, adaptor, cacheGetChannel, taskId, taskM); err != nil { logger.LogError(ctx, fmt.Sprintf("Failed to update video task %s: %s", taskId, err.Error())) } - // sleep 1 second between each task to avoid hitting rate limits of upstream platforms + if disablePollingSleep || i == len(taskIds)-1 { + continue + } + + // sleep 1 second between tasks for this channel only. select { case <-ctx.Done(): return ctx.Err() diff --git a/service/task_polling_test.go b/service/task_polling_test.go new file mode 100644 index 00000000..3164d0a9 --- /dev/null +++ b/service/task_polling_test.go @@ -0,0 +1,333 @@ +package service + +import ( + "bytes" + "context" + "io" + "net/http" + "sync" + "testing" + "time" + + "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/constant" + "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/model" + relaycommon "github.com/QuantumNous/new-api/relay/common" + "github.com/bytedance/gopkg/util/gopool" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type taskPollingFetchAdaptor struct { + mu sync.Mutex + taskIDs []string + fetched chan string + blockTaskID string + blockStarted chan struct{} + releaseBlock chan struct{} + blockOnce sync.Once +} + +func (a *taskPollingFetchAdaptor) Init(_ *relaycommon.RelayInfo) {} + +func (a *taskPollingFetchAdaptor) FetchTask(_ string, _ string, body map[string]any, _ string) (*http.Response, error) { + taskID, _ := body["task_id"].(string) + if taskID == a.blockTaskID && a.releaseBlock != nil { + a.blockOnce.Do(func() { + if a.blockStarted != nil { + close(a.blockStarted) + } + }) + <-a.releaseBlock + } + + a.mu.Lock() + a.taskIDs = append(a.taskIDs, taskID) + a.mu.Unlock() + if a.fetched != nil { + select { + case a.fetched <- taskID: + default: + } + } + + response := dto.TaskResponse[model.Task]{ + Code: dto.TaskSuccessCode, + Data: model.Task{ + TaskID: taskID, + Status: model.TaskStatusInProgress, + Progress: "30%", + }, + } + responseBody, err := common.Marshal(response) + if err != nil { + return nil, err + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewReader(responseBody)), + }, nil +} + +func (a *taskPollingFetchAdaptor) ParseTaskResult([]byte) (*relaycommon.TaskInfo, error) { + return &relaycommon.TaskInfo{Status: model.TaskStatusInProgress}, nil +} + +func (a *taskPollingFetchAdaptor) AdjustBillingOnComplete(_ *model.Task, _ *relaycommon.TaskInfo) int { + return 0 +} + +func (a *taskPollingFetchAdaptor) fetchCount() int { + a.mu.Lock() + defer a.mu.Unlock() + return len(a.taskIDs) +} + +func (a *taskPollingFetchAdaptor) fetchedTaskIDs() []string { + a.mu.Lock() + defer a.mu.Unlock() + return append([]string(nil), a.taskIDs...) +} + +func seedTaskPollingChannel(t *testing.T, id int, disableSleep bool) { + t.Helper() + ch := &model.Channel{ + Id: id, + Type: constant.ChannelTypeKling, + Name: "polling_channel", + Key: "sk-test", + Status: common.ChannelStatusEnabled, + } + if disableSleep { + ch.SetOtherSettings(dto.ChannelOtherSettings{DisableTaskPollingSleep: true}) + } + require.NoError(t, model.DB.Create(ch).Error) +} + +func seedPollingTask(t *testing.T, channelID int, publicID string, upstreamID string) *model.Task { + t.Helper() + task := &model.Task{ + TaskID: publicID, + Platform: constant.TaskPlatform("kling"), + UserId: 1, + ChannelId: channelID, + Action: constant.TaskActionGenerate, + Status: model.TaskStatusInProgress, + Progress: "30%", + CreatedAt: time.Now().Unix(), + UpdatedAt: time.Now().Unix(), + PrivateData: model.TaskPrivateData{ + UpstreamTaskID: upstreamID, + }, + } + require.NoError(t, model.DB.Create(task).Error) + return task +} + +func TestUpdateVideoTasksDefaultSleepWaitsBetweenTasks(t *testing.T) { + truncate(t) + + const channelID = 101 + seedTaskPollingChannel(t, channelID, false) + first := seedPollingTask(t, channelID, "task_public_1", "upstream_1") + second := seedPollingTask(t, channelID, "task_public_2", "upstream_2") + + adaptor := &taskPollingFetchAdaptor{} + previousFactory := GetTaskAdaptorFunc + GetTaskAdaptorFunc = func(constant.TaskPlatform) TaskPollingAdaptor { return adaptor } + t.Cleanup(func() { GetTaskAdaptorFunc = previousFactory }) + + ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) + defer cancel() + + err := UpdateVideoTasks(ctx, constant.TaskPlatform("kling"), map[int][]string{ + channelID: { + first.GetUpstreamTaskID(), + second.GetUpstreamTaskID(), + }, + }, map[string]*model.Task{ + first.GetUpstreamTaskID(): first, + second.GetUpstreamTaskID(): second, + }) + + require.ErrorIs(t, err, context.DeadlineExceeded) + assert.Equal(t, 1, adaptor.fetchCount()) +} + +func TestUpdateVideoTasksCanSkipPollingSleepPerChannel(t *testing.T) { + truncate(t) + + const channelID = 102 + seedTaskPollingChannel(t, channelID, true) + first := seedPollingTask(t, channelID, "task_public_3", "upstream_3") + second := seedPollingTask(t, channelID, "task_public_4", "upstream_4") + + adaptor := &taskPollingFetchAdaptor{} + previousFactory := GetTaskAdaptorFunc + GetTaskAdaptorFunc = func(constant.TaskPlatform) TaskPollingAdaptor { return adaptor } + t.Cleanup(func() { GetTaskAdaptorFunc = previousFactory }) + + ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) + defer cancel() + + err := UpdateVideoTasks(ctx, constant.TaskPlatform("kling"), map[int][]string{ + channelID: { + first.GetUpstreamTaskID(), + second.GetUpstreamTaskID(), + }, + }, map[string]*model.Task{ + first.GetUpstreamTaskID(): first, + second.GetUpstreamTaskID(): second, + }) + + require.NoError(t, err) + assert.Equal(t, 2, adaptor.fetchCount()) +} + +func TestUpdateVideoTasksDefaultSleepDoesNotBlockOtherChannels(t *testing.T) { + truncate(t) + + const firstChannelID = 201 + const secondChannelID = 202 + seedTaskPollingChannel(t, firstChannelID, false) + seedTaskPollingChannel(t, secondChannelID, false) + firstChannelFirst := seedPollingTask(t, firstChannelID, "task_public_5", "upstream_a_1") + firstChannelSecond := seedPollingTask(t, firstChannelID, "task_public_6", "upstream_a_2") + secondChannelFirst := seedPollingTask(t, secondChannelID, "task_public_7", "upstream_b_1") + secondChannelSecond := seedPollingTask(t, secondChannelID, "task_public_8", "upstream_b_2") + + adaptor := &taskPollingFetchAdaptor{} + previousFactory := GetTaskAdaptorFunc + GetTaskAdaptorFunc = func(constant.TaskPlatform) TaskPollingAdaptor { return adaptor } + t.Cleanup(func() { GetTaskAdaptorFunc = previousFactory }) + + ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) + defer cancel() + + err := UpdateVideoTasks(ctx, constant.TaskPlatform("kling"), map[int][]string{ + firstChannelID: { + firstChannelFirst.GetUpstreamTaskID(), + firstChannelSecond.GetUpstreamTaskID(), + }, + secondChannelID: { + secondChannelFirst.GetUpstreamTaskID(), + secondChannelSecond.GetUpstreamTaskID(), + }, + }, map[string]*model.Task{ + firstChannelFirst.GetUpstreamTaskID(): firstChannelFirst, + firstChannelSecond.GetUpstreamTaskID(): firstChannelSecond, + secondChannelFirst.GetUpstreamTaskID(): secondChannelFirst, + secondChannelSecond.GetUpstreamTaskID(): secondChannelSecond, + }) + + require.ErrorIs(t, err, context.DeadlineExceeded) + assert.ElementsMatch(t, []string{"upstream_a_1", "upstream_b_1"}, adaptor.fetchedTaskIDs()) +} + +func TestUpdateVideoTasksSlowChannelDoesNotBlockOtherChannels(t *testing.T) { + truncate(t) + + const slowChannelID = 251 + const fastChannelID = 252 + seedTaskPollingChannel(t, slowChannelID, false) + seedTaskPollingChannel(t, fastChannelID, true) + slowTask := seedPollingTask(t, slowChannelID, "task_public_slow", "upstream_slow_1") + fastFirst := seedPollingTask(t, fastChannelID, "task_public_fast_1", "upstream_fast_parallel_1") + fastSecond := seedPollingTask(t, fastChannelID, "task_public_fast_2", "upstream_fast_parallel_2") + + adaptor := &taskPollingFetchAdaptor{ + fetched: make(chan string, 4), + blockTaskID: slowTask.GetUpstreamTaskID(), + blockStarted: make(chan struct{}), + releaseBlock: make(chan struct{}), + } + var releaseOnce sync.Once + releaseBlockedTask := func() { + releaseOnce.Do(func() { + close(adaptor.releaseBlock) + }) + } + t.Cleanup(releaseBlockedTask) + previousFactory := GetTaskAdaptorFunc + GetTaskAdaptorFunc = func(constant.TaskPlatform) TaskPollingAdaptor { return adaptor } + t.Cleanup(func() { GetTaskAdaptorFunc = previousFactory }) + + errCh := make(chan error, 1) + gopool.Go(func() { + errCh <- UpdateVideoTasks(context.Background(), constant.TaskPlatform("kling"), map[int][]string{ + slowChannelID: { + slowTask.GetUpstreamTaskID(), + }, + fastChannelID: { + fastFirst.GetUpstreamTaskID(), + fastSecond.GetUpstreamTaskID(), + }, + }, map[string]*model.Task{ + slowTask.GetUpstreamTaskID(): slowTask, + fastFirst.GetUpstreamTaskID(): fastFirst, + fastSecond.GetUpstreamTaskID(): fastSecond, + }) + }) + + select { + case <-adaptor.blockStarted: + case <-time.After(500 * time.Millisecond): + t.Fatal("slow channel did not start blocking") + } + + require.Eventually(t, func() bool { + fetchedTaskIDs := adaptor.fetchedTaskIDs() + return len(fetchedTaskIDs) == 2 && + fetchedTaskIDs[0] == fastFirst.GetUpstreamTaskID() && + fetchedTaskIDs[1] == fastSecond.GetUpstreamTaskID() + }, 500*time.Millisecond, 10*time.Millisecond) + + releaseBlockedTask() + require.NoError(t, <-errCh) + assert.ElementsMatch(t, []string{ + slowTask.GetUpstreamTaskID(), + fastFirst.GetUpstreamTaskID(), + fastSecond.GetUpstreamTaskID(), + }, adaptor.fetchedTaskIDs()) +} + +func TestUpdateVideoTasksMixedChannelSleepSettings(t *testing.T) { + truncate(t) + + const sleepyChannelID = 301 + const fastChannelID = 302 + seedTaskPollingChannel(t, sleepyChannelID, false) + seedTaskPollingChannel(t, fastChannelID, true) + sleepyFirst := seedPollingTask(t, sleepyChannelID, "task_public_9", "upstream_sleepy_1") + sleepySecond := seedPollingTask(t, sleepyChannelID, "task_public_10", "upstream_sleepy_2") + fastFirst := seedPollingTask(t, fastChannelID, "task_public_11", "upstream_fast_1") + fastSecond := seedPollingTask(t, fastChannelID, "task_public_12", "upstream_fast_2") + + adaptor := &taskPollingFetchAdaptor{} + previousFactory := GetTaskAdaptorFunc + GetTaskAdaptorFunc = func(constant.TaskPlatform) TaskPollingAdaptor { return adaptor } + t.Cleanup(func() { GetTaskAdaptorFunc = previousFactory }) + + ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) + defer cancel() + + err := UpdateVideoTasks(ctx, constant.TaskPlatform("kling"), map[int][]string{ + sleepyChannelID: { + sleepyFirst.GetUpstreamTaskID(), + sleepySecond.GetUpstreamTaskID(), + }, + fastChannelID: { + fastFirst.GetUpstreamTaskID(), + fastSecond.GetUpstreamTaskID(), + }, + }, map[string]*model.Task{ + sleepyFirst.GetUpstreamTaskID(): sleepyFirst, + sleepySecond.GetUpstreamTaskID(): sleepySecond, + fastFirst.GetUpstreamTaskID(): fastFirst, + fastSecond.GetUpstreamTaskID(): fastSecond, + }) + + require.ErrorIs(t, err, context.DeadlineExceeded) + assert.ElementsMatch(t, []string{"upstream_sleepy_1", "upstream_fast_1", "upstream_fast_2"}, adaptor.fetchedTaskIDs()) +} diff --git a/web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx b/web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx index a38ec0af..cab16261 100644 --- a/web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx +++ b/web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx @@ -3249,6 +3249,31 @@ export function ChannelMutateDrawer({ )} /> + + ( + +
+ + {t('Skip async task polling delay')} + + + {t( + 'Do not wait one second between polling async tasks for this channel' + )} + +
+ + + +
+ )} + /> >([ 'allow_inference_geo', 'allow_speed', 'claude_beta_query', + 'disable_task_polling_sleep', 'upstream_model_update_check_enabled', 'upstream_model_update_auto_sync_enabled', 'upstream_model_update_ignored_models', diff --git a/web/default/src/features/channels/lib/channel-form.ts b/web/default/src/features/channels/lib/channel-form.ts index 3a043153..02c8fc77 100644 --- a/web/default/src/features/channels/lib/channel-form.ts +++ b/web/default/src/features/channels/lib/channel-form.ts @@ -203,6 +203,7 @@ export const channelFormSchema = z allow_inference_geo: z.boolean().optional(), // OpenAI/Anthropic: inference geography allow_speed: z.boolean().optional(), // Anthropic: speed mode control claude_beta_query: z.boolean().optional(), // Anthropic: beta query passthrough + disable_task_polling_sleep: z.boolean().optional(), // Upstream model update settings (stored in settings JSON) upstream_model_update_check_enabled: z.boolean().optional(), upstream_model_update_auto_sync_enabled: z.boolean().optional(), @@ -342,6 +343,7 @@ export const CHANNEL_FORM_DEFAULT_VALUES: ChannelFormValues = { allow_inference_geo: false, allow_speed: false, claude_beta_query: false, + disable_task_polling_sleep: false, upstream_model_update_check_enabled: false, upstream_model_update_auto_sync_enabled: false, upstream_model_update_ignored_models: '', @@ -397,6 +399,7 @@ export function transformChannelToFormDefaults( let allowInferenceGeo = false let allowSpeed = false let claudeBetaQuery = false + let disableTaskPollingSleep = false let upstreamModelUpdateCheckEnabled = false let upstreamModelUpdateAutoSyncEnabled = false let upstreamModelUpdateIgnoredModels = '' @@ -416,6 +419,7 @@ export function transformChannelToFormDefaults( allowInferenceGeo = parsed.allow_inference_geo === true allowSpeed = parsed.allow_speed === true claudeBetaQuery = parsed.claude_beta_query === true + disableTaskPollingSleep = parsed.disable_task_polling_sleep === true upstreamModelUpdateCheckEnabled = parsed.upstream_model_update_check_enabled === true upstreamModelUpdateAutoSyncEnabled = @@ -473,6 +477,7 @@ export function transformChannelToFormDefaults( allow_inference_geo: allowInferenceGeo, allow_speed: allowSpeed, claude_beta_query: claudeBetaQuery, + disable_task_polling_sleep: disableTaskPollingSleep, allow_safety_identifier: allowSafetyIdentifier, upstream_model_update_check_enabled: upstreamModelUpdateCheckEnabled, upstream_model_update_auto_sync_enabled: upstreamModelUpdateAutoSyncEnabled, @@ -576,6 +581,9 @@ function buildSettingsJSON(formData: ChannelFormValues): string { if ('claude_beta_query' in settingsObj) delete settingsObj.claude_beta_query } + settingsObj.disable_task_polling_sleep = + formData.disable_task_polling_sleep === true + // Upstream model update settings (for model-fetchable channel types) if (MODEL_FETCHABLE_TYPES.has(formData.type)) { settingsObj.upstream_model_update_check_enabled = diff --git a/web/default/src/features/channels/types.ts b/web/default/src/features/channels/types.ts index 54879d89..a70d5a28 100644 --- a/web/default/src/features/channels/types.ts +++ b/web/default/src/features/channels/types.ts @@ -100,6 +100,7 @@ export interface ChannelOtherSettings { allow_inference_geo?: boolean allow_speed?: boolean claude_beta_query?: boolean + disable_task_polling_sleep?: boolean upstream_model_update_check_enabled?: boolean upstream_model_update_auto_sync_enabled?: boolean upstream_model_update_ignored_models?: string[] diff --git a/web/default/src/i18n/locales/en.json b/web/default/src/i18n/locales/en.json index 432bc6dd..01df6215 100644 --- a/web/default/src/i18n/locales/en.json +++ b/web/default/src/i18n/locales/en.json @@ -1350,6 +1350,7 @@ "Displays the mobile sidebar.": "Displays the mobile sidebar.", "Do not over-trust this feature. IP may be spoofed. Please use with nginx, CDN and other gateways.": "Do not over-trust this feature. IP may be spoofed. Please use with nginx, CDN and other gateways.", "Do not repeat check-in; only once per day": "Do not repeat check-in; only once per day", + "Do not wait one second between polling async tasks for this channel": "Do not wait one second between polling async tasks for this channel", "Do regex replacement in the target field": "Do regex replacement in the target field", "Do string replacement in the target field": "Do string replacement in the target field", "Docs": "Docs", @@ -3962,6 +3963,7 @@ "Simple mode only returns message; status code and error type use system defaults.": "Simple mode only returns message; status code and error type use system defaults.", "Simple mode: prune objects by type, e.g. redacted_thinking.": "Simple mode: prune objects by type, e.g. redacted_thinking.", "Single Key": "Single Key", + "Skip async task polling delay": "Skip async task polling delay", "Site & Branding": "Site & Branding", "Site Key": "Site Key", "Size:": "Size:", diff --git a/web/default/src/i18n/locales/fr.json b/web/default/src/i18n/locales/fr.json index cd4f2694..27fa244b 100644 --- a/web/default/src/i18n/locales/fr.json +++ b/web/default/src/i18n/locales/fr.json @@ -1350,6 +1350,7 @@ "Displays the mobile sidebar.": "Affiche la barre latérale mobile.", "Do not over-trust this feature. IP may be spoofed. Please use with nginx, CDN and other gateways.": "Ne faites pas trop confiance à cette fonctionnalité. L'IP peut être usurpée. Veuillez l'utiliser avec nginx, CDN et autres passerelles.", "Do not repeat check-in; only once per day": "Ne répétez pas le check-in ; une seule fois par jour", + "Do not wait one second between polling async tasks for this channel": "Ne pas attendre une seconde entre les interrogations des tâches asynchrones pour ce canal", "Do regex replacement in the target field": "Effectuer un remplacement par expression régulière dans le champ cible", "Do string replacement in the target field": "Effectuer un remplacement de chaîne dans le champ cible", "Docs": "Documents", @@ -3962,6 +3963,7 @@ "Simple mode only returns message; status code and error type use system defaults.": "Le mode simple ne retourne que le message ; le code de statut et le type d'erreur utilisent les valeurs par défaut.", "Simple mode: prune objects by type, e.g. redacted_thinking.": "Mode simple : nettoyer les objets par type, ex. redacted_thinking.", "Single Key": "Clé unique", + "Skip async task polling delay": "Ignorer le délai de polling des tâches asynchrones", "Site & Branding": "Site et marque", "Site Key": "Clé du site", "Size:": "Taille :", diff --git a/web/default/src/i18n/locales/ja.json b/web/default/src/i18n/locales/ja.json index 827bc498..9eb3e17b 100644 --- a/web/default/src/i18n/locales/ja.json +++ b/web/default/src/i18n/locales/ja.json @@ -1350,6 +1350,7 @@ "Displays the mobile sidebar.": "モバイルサイドバーを表示します。", "Do not over-trust this feature. IP may be spoofed. Please use with nginx, CDN and other gateways.": "この機能を過信しないでください。IPは偽装される可能性があります。nginx、CDNなどのゲートウェイと併用してください。", "Do not repeat check-in; only once per day": "チェックインを繰り返さないでください;1日1回のみ", + "Do not wait one second between polling async tasks for this channel": "このチャネルの非同期タスクをポーリングする間に1秒待機しない", "Do regex replacement in the target field": "ターゲットフィールドで正規表現置換", "Do string replacement in the target field": "ターゲットフィールドで文字列置換", "Docs": "ドキュメント", @@ -3962,6 +3963,7 @@ "Simple mode only returns message; status code and error type use system defaults.": "シンプルモードはメッセージのみ返します。ステータスコードとエラータイプはシステムデフォルトを使用します。", "Simple mode: prune objects by type, e.g. redacted_thinking.": "シンプルモード:typeでオブジェクトを削除(例:redacted_thinking)。", "Single Key": "単一キー", + "Skip async task polling delay": "非同期タスクのポーリング遅延をスキップ", "Site & Branding": "サイトとブランド", "Site Key": "サイトキー", "Size:": "サイズ:", diff --git a/web/default/src/i18n/locales/ru.json b/web/default/src/i18n/locales/ru.json index f3d8c29e..096ee9cc 100644 --- a/web/default/src/i18n/locales/ru.json +++ b/web/default/src/i18n/locales/ru.json @@ -1350,6 +1350,7 @@ "Displays the mobile sidebar.": "Отображает мобильную боковую панель.", "Do not over-trust this feature. IP may be spoofed. Please use with nginx, CDN and other gateways.": "Не доверяйте этой функции слишком сильно. IP может быть подделан. Используйте с nginx, CDN и другими шлюзами.", "Do not repeat check-in; only once per day": "Не повторяйте отметку; только один раз в день", + "Do not wait one second between polling async tasks for this channel": "Не ждать одну секунду между опросами асинхронных задач для этого канала", "Do regex replacement in the target field": "Выполнить замену по регулярному выражению в целевом поле", "Do string replacement in the target field": "Выполнить замену строки в целевом поле", "Docs": "Документы", @@ -3962,6 +3963,7 @@ "Simple mode only returns message; status code and error type use system defaults.": "Простой режим возвращает только сообщение; код статуса и тип ошибки используют системные значения по умолчанию.", "Simple mode: prune objects by type, e.g. redacted_thinking.": "Простой режим: очистка объектов по типу, например redacted_thinking.", "Single Key": "Одиночный ключ", + "Skip async task polling delay": "Пропускать задержку опроса асинхронных задач", "Site & Branding": "Сайт и брендинг", "Site Key": "Ключ сайта", "Size:": "Размер:", diff --git a/web/default/src/i18n/locales/vi.json b/web/default/src/i18n/locales/vi.json index cba49a7e..d3015d64 100644 --- a/web/default/src/i18n/locales/vi.json +++ b/web/default/src/i18n/locales/vi.json @@ -1350,6 +1350,7 @@ "Displays the mobile sidebar.": "Hiển thị thanh bên di động.", "Do not over-trust this feature. IP may be spoofed. Please use with nginx, CDN and other gateways.": "Đừng tin tưởng quá mức vào tính năng này. IP có thể bị giả mạo. Hãy sử dụng cùng với nginx, CDN và các gateway khác.", "Do not repeat check-in; only once per day": "Không lặp lại check-in; chỉ một lần mỗi ngày", + "Do not wait one second between polling async tasks for this channel": "Không chờ một giây giữa các lần thăm dò tác vụ bất đồng bộ cho kênh này", "Do regex replacement in the target field": "Thực hiện thay thế regex trong trường đích", "Do string replacement in the target field": "Thực hiện thay thế chuỗi trong trường đích", "Docs": "Tài liệu", @@ -3962,6 +3963,7 @@ "Simple mode only returns message; status code and error type use system defaults.": "Chế độ đơn giản chỉ trả về message; mã trạng thái và loại lỗi sử dụng giá trị mặc định.", "Simple mode: prune objects by type, e.g. redacted_thinking.": "Chế độ đơn giản: dọn dẹp đối tượng theo type, ví dụ redacted_thinking.", "Single Key": "Khóa đơn", + "Skip async task polling delay": "Bỏ qua độ trễ thăm dò tác vụ bất đồng bộ", "Site & Branding": "Trang web & thương hiệu", "Site Key": "Khóa trang web", "Size:": "Kích thước:", diff --git a/web/default/src/i18n/locales/zh.json b/web/default/src/i18n/locales/zh.json index b8fbb492..44ba4f4c 100644 --- a/web/default/src/i18n/locales/zh.json +++ b/web/default/src/i18n/locales/zh.json @@ -1350,6 +1350,7 @@ "Displays the mobile sidebar.": "显示移动侧边栏。", "Do not over-trust this feature. IP may be spoofed. Please use with nginx, CDN and other gateways.": "请勿过度信任此功能,IP 可能被伪造,请配合 nginx 和 cdn 等网关使用", "Do not repeat check-in; only once per day": "请勿重复签到;每天仅一次", + "Do not wait one second between polling async tasks for this channel": "该渠道轮询异步任务时不等待一秒", "Do regex replacement in the target field": "在目标字段里做正则替换", "Do string replacement in the target field": "在目标字段里做字符串替换", "Docs": "文档", @@ -3962,6 +3963,7 @@ "Simple mode only returns message; status code and error type use system defaults.": "简洁模式仅返回 message;状态码和错误类型将使用系统默认值。", "Simple mode: prune objects by type, e.g. redacted_thinking.": "简洁模式:按 type 全量清理对象,例如 redacted_thinking。", "Single Key": "单密钥", + "Skip async task polling delay": "跳过异步任务轮询延迟", "Site & Branding": "站点与品牌", "Site Key": "站点密钥", "Size:": "大小:",