mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-08 10:27:46 +08:00
fix(routing): harden routed pool scheduling
This commit is contained in:
@@ -530,7 +530,8 @@ import { CircleHelp } from 'lucide-vue-next'
|
||||
import { Dialog, Button, Input, Label, Switch, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import { updateProvider } from '@/api/endpoints'
|
||||
import { getProvider, updateProvider } from '@/api/endpoints'
|
||||
import { mergePoolAdvancedPatch } from '@/features/pool/utils/poolSchedulingDialog'
|
||||
import {
|
||||
buildPoolCooldownFieldLayout,
|
||||
buildPoolHealthToggleCards,
|
||||
@@ -557,6 +558,7 @@ const emit = defineEmits<{
|
||||
|
||||
const { success, error: showError } = useToast()
|
||||
const loading = ref(false)
|
||||
let dialogRevision = 0
|
||||
|
||||
const isClaudeCode = computed(() => {
|
||||
return (props.providerType || '').trim().toLowerCase() === 'claude_code'
|
||||
@@ -652,7 +654,9 @@ function updateHealthToggleValue(key: PoolHealthToggleKey, value: boolean): void
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.modelValue, (open) => {
|
||||
watch([() => props.modelValue, () => props.providerId], ([open]) => {
|
||||
dialogRevision += 1
|
||||
loading.value = false
|
||||
if (!open) return
|
||||
|
||||
const cfg = props.currentConfig
|
||||
@@ -698,12 +702,26 @@ watch(() => props.modelValue, (open) => {
|
||||
})
|
||||
|
||||
async function handleSave() {
|
||||
const providerId = props.providerId
|
||||
const revision = dialogRevision
|
||||
loading.value = true
|
||||
try {
|
||||
const latestProvider = await getProvider(providerId)
|
||||
if (!props.modelValue || props.providerId !== providerId || dialogRevision !== revision) return
|
||||
const latestAdvanced = (latestProvider as Record<string, unknown>).pool_advanced
|
||||
const existingPoolAdvanced = mergePoolAdvancedPatch(latestAdvanced, {})
|
||||
const latestScoreRules = typeof existingPoolAdvanced.score_rules === 'object'
|
||||
&& existingPoolAdvanced.score_rules !== null
|
||||
? existingPoolAdvanced.score_rules as Record<string, unknown>
|
||||
: {}
|
||||
const latestScoreWeights = typeof latestScoreRules.weights === 'object'
|
||||
&& latestScoreRules.weights !== null
|
||||
? latestScoreRules.weights as Record<string, unknown>
|
||||
: {}
|
||||
const scoreRules = {
|
||||
...(props.currentConfig?.score_rules ?? {}),
|
||||
...latestScoreRules,
|
||||
weights: {
|
||||
...(props.currentConfig?.score_rules?.weights ?? {}),
|
||||
...latestScoreWeights,
|
||||
manual_priority: form.value.score_weight_manual_priority ?? undefined,
|
||||
health: form.value.score_weight_health ?? undefined,
|
||||
probe_freshness: form.value.score_weight_probe_freshness ?? undefined,
|
||||
@@ -717,31 +735,8 @@ async function handleSave() {
|
||||
request_failure_penalty: form.value.request_failure_penalty ?? undefined,
|
||||
probe_failure_cooldown_threshold: form.value.probe_failure_cooldown_threshold ?? undefined,
|
||||
}
|
||||
const existingPoolAdvanced: Record<string, unknown> = { ...(props.currentConfig ?? {}) }
|
||||
for (const key of [
|
||||
'probing_target_percent',
|
||||
'probing_target_count',
|
||||
'probing_active_target_percent',
|
||||
'probing_active_target_count',
|
||||
'active_probe_target_percent',
|
||||
'active_probe_target_count',
|
||||
'probing_interval_minutes',
|
||||
'account_self_check_method',
|
||||
'self_check_method',
|
||||
'account_self_check_request',
|
||||
'self_check_request',
|
||||
'health_policy_enabled',
|
||||
'sticky_session_ttl_seconds',
|
||||
'global_priority',
|
||||
'cost_window_seconds',
|
||||
'cost_limit_per_key_tokens',
|
||||
'cost_soft_threshold_percent',
|
||||
]) {
|
||||
delete existingPoolAdvanced[key]
|
||||
}
|
||||
// 合并已有配置(保留 scheduling_presets 等不在此对话框编辑的字段)
|
||||
const poolAdvanced: Record<string, unknown> = {
|
||||
...existingPoolAdvanced,
|
||||
const poolAdvanced = mergePoolAdvancedPatch(existingPoolAdvanced, {
|
||||
rate_limit_cooldown_seconds: form.value.rate_limit_cooldown_seconds ?? undefined,
|
||||
overload_cooldown_seconds: form.value.overload_cooldown_seconds ?? undefined,
|
||||
batch_concurrency: form.value.batch_concurrency ?? undefined,
|
||||
@@ -760,7 +755,7 @@ async function handleSave() {
|
||||
auto_remove_banned_keys: form.value.auto_remove_banned_keys,
|
||||
auto_remove_quota_exhausted_keys: form.value.auto_remove_quota_exhausted_keys,
|
||||
skip_exhausted_accounts: form.value.skip_exhausted_accounts,
|
||||
}
|
||||
})
|
||||
|
||||
const payload: Parameters<typeof updateProvider>[1] = {
|
||||
pool_advanced: poolAdvanced as PoolAdvancedConfig,
|
||||
@@ -776,14 +771,17 @@ async function handleSave() {
|
||||
cli_only_enabled: cf.cli_only_enabled,
|
||||
}
|
||||
}
|
||||
const updatedProvider = await updateProvider(props.providerId, payload)
|
||||
const updatedProvider = await updateProvider(providerId, payload)
|
||||
if (!props.modelValue || props.providerId !== providerId || dialogRevision !== revision) return
|
||||
success('高级设置已保存')
|
||||
emit('saved', updatedProvider)
|
||||
emit('update:modelValue', false)
|
||||
} catch (err) {
|
||||
showError(parseApiError(err))
|
||||
} finally {
|
||||
loading.value = false
|
||||
if (dialogRevision === revision) {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -218,9 +218,13 @@ import { GripVertical } from 'lucide-vue-next'
|
||||
import { Dialog, Button, Switch } from '@/components/ui'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import { updateProvider } from '@/api/endpoints'
|
||||
import { getProvider, updateProvider } from '@/api/endpoints'
|
||||
import { getPoolSchedulingPresets } from '@/api/endpoints/pool'
|
||||
import { moveStrategyItem } from '@/features/pool/utils/poolSchedulingDialog'
|
||||
import {
|
||||
mergePoolAdvancedPatch,
|
||||
moveStrategyItem,
|
||||
normalizeMutexSelection,
|
||||
} from '@/features/pool/utils/poolSchedulingDialog'
|
||||
import type { PoolPresetMeta } from '@/api/endpoints/pool'
|
||||
import type {
|
||||
PoolAdvancedConfig,
|
||||
@@ -267,6 +271,7 @@ const FALLBACK_PRESET_DEFS: PoolPresetMeta[] = [
|
||||
mutex_group: DISTRIBUTION_GROUP,
|
||||
evidence_hint: '依据 LRU 时间戳(最近使用优先,与 LRU 轮转相反)',
|
||||
providers: [],
|
||||
default_enabled: true,
|
||||
modes: null,
|
||||
default_mode: null,
|
||||
},
|
||||
@@ -300,12 +305,25 @@ const FALLBACK_PRESET_DEFS: PoolPresetMeta[] = [
|
||||
modes: null,
|
||||
default_mode: null,
|
||||
},
|
||||
{
|
||||
name: 'free_team_first',
|
||||
label: 'Free/Team 优先',
|
||||
description: '兼容旧配置:优先消耗 Free、Team 或两者',
|
||||
evidence_hint: '依据 plan_type,保留旧 free_only/team_only/both 语义',
|
||||
providers: ['codex', 'grok', 'kiro', 'windsurf'],
|
||||
modes: [
|
||||
{ value: 'free_only', label: 'Free' },
|
||||
{ value: 'team_only', label: 'Team' },
|
||||
{ value: 'both', label: 'Free + Team' },
|
||||
],
|
||||
default_mode: 'both',
|
||||
},
|
||||
{
|
||||
name: 'free_first',
|
||||
label: 'Free 优先',
|
||||
description: '优先消耗 Free 账号(依赖 plan_type)',
|
||||
evidence_hint: '依据 plan_type(Free 账号优先调度)',
|
||||
providers: ['codex', 'kiro'],
|
||||
providers: ['codex', 'grok', 'kiro', 'windsurf'],
|
||||
modes: null,
|
||||
default_mode: null,
|
||||
},
|
||||
@@ -314,7 +332,7 @@ const FALLBACK_PRESET_DEFS: PoolPresetMeta[] = [
|
||||
label: 'Team 优先',
|
||||
description: '优先消耗 Team 账号(依赖 plan_type)',
|
||||
evidence_hint: '依据 plan_type(Team 账号优先调度)',
|
||||
providers: ['codex', 'kiro'],
|
||||
providers: ['codex', 'grok', 'kiro', 'windsurf'],
|
||||
modes: null,
|
||||
default_mode: null,
|
||||
},
|
||||
@@ -323,7 +341,7 @@ const FALLBACK_PRESET_DEFS: PoolPresetMeta[] = [
|
||||
label: 'Plus 优先',
|
||||
description: '优先消耗 Plus 账号(依赖 plan_type)',
|
||||
evidence_hint: '依据 plan_type(Plus 账号优先调度)',
|
||||
providers: ['codex', 'kiro'],
|
||||
providers: ['codex', 'grok', 'kiro', 'windsurf'],
|
||||
modes: null,
|
||||
default_mode: null,
|
||||
},
|
||||
@@ -332,7 +350,7 @@ const FALLBACK_PRESET_DEFS: PoolPresetMeta[] = [
|
||||
label: 'Pro 优先',
|
||||
description: '优先消耗 Pro 账号(依赖 plan_type)',
|
||||
evidence_hint: '依据 plan_type(Pro 账号优先调度)',
|
||||
providers: ['codex', 'kiro'],
|
||||
providers: ['codex', 'grok', 'kiro', 'windsurf'],
|
||||
modes: null,
|
||||
default_mode: null,
|
||||
},
|
||||
@@ -350,7 +368,8 @@ const FALLBACK_PRESET_DEFS: PoolPresetMeta[] = [
|
||||
label: '额度刷新优先',
|
||||
description: '优先选即将刷新额度的账号',
|
||||
evidence_hint: '依据账号额度重置倒计时(next_reset / reset_seconds)',
|
||||
providers: ['codex', 'kiro'],
|
||||
providers: ['codex', 'grok', 'kiro', 'windsurf'],
|
||||
default_enabled_providers: ['codex', 'windsurf'],
|
||||
modes: null,
|
||||
default_mode: null,
|
||||
},
|
||||
@@ -392,10 +411,9 @@ const FALLBACK_PRESET_DEFS: PoolPresetMeta[] = [
|
||||
},
|
||||
]
|
||||
|
||||
const DEFAULT_ENABLED_PRESETS = new Set(['cache_affinity', 'recent_refresh'])
|
||||
|
||||
const { success, error: showError } = useToast()
|
||||
const loading = ref(false)
|
||||
let dialogRevision = 0
|
||||
const presetDefs = ref<PoolPresetMeta[]>([])
|
||||
const presetDefsLoaded = ref(false)
|
||||
const loadingPresetDefs = ref(false)
|
||||
@@ -443,11 +461,16 @@ function normalizePresetDefs(defs: PoolPresetMeta[]): PoolPresetMeta[] {
|
||||
.filter(mode => Boolean(mode.value))
|
||||
: null
|
||||
const defaultMode = normalizeMode(raw.default_mode)
|
||||
const defaultEnabledProviders = Array.isArray(raw.default_enabled_providers)
|
||||
? raw.default_enabled_providers.map(p => normalizeProviderType(p)).filter(Boolean)
|
||||
: []
|
||||
ordered.push({
|
||||
name,
|
||||
label: String(raw.label ?? '').trim() || name,
|
||||
description: String(raw.description ?? '').trim(),
|
||||
providers,
|
||||
default_enabled: raw.default_enabled === true,
|
||||
default_enabled_providers: defaultEnabledProviders,
|
||||
modes: modes && modes.length > 0 ? modes : null,
|
||||
default_mode: defaultMode,
|
||||
mutex_group: normalizeMutexGroup(raw.mutex_group),
|
||||
@@ -478,11 +501,11 @@ async function ensurePresetDefsLoaded(): Promise<void> {
|
||||
const normalized = normalizePresetDefs(Array.isArray(remoteDefs) ? remoteDefs : [])
|
||||
if (normalized.length > 0) {
|
||||
presetDefs.value = normalized
|
||||
presetDefsLoaded.value = true
|
||||
}
|
||||
} catch (err) {
|
||||
showError(parseApiError(err))
|
||||
} finally {
|
||||
presetDefsLoaded.value = true
|
||||
loadingPresetDefs.value = false
|
||||
}
|
||||
}
|
||||
@@ -530,7 +553,15 @@ function buildPresetListItem(def: PoolPresetMeta, enabled: boolean, mode?: unkno
|
||||
}
|
||||
|
||||
function buildDefaultPresetList(): PresetListItem[] {
|
||||
return getPresetDefs().map(def => buildPresetListItem(def, DEFAULT_ENABLED_PRESETS.has(def.name)))
|
||||
const providerType = normalizeProviderType(props.providerType)
|
||||
return getPresetDefs().map((def) => {
|
||||
const providerDefaults = Array.isArray(def.default_enabled_providers)
|
||||
? def.default_enabled_providers.map(normalizeProviderType)
|
||||
: []
|
||||
const enabled = def.default_enabled === true
|
||||
|| (Boolean(providerType) && providerDefaults.includes(providerType))
|
||||
return buildPresetListItem(def, enabled)
|
||||
})
|
||||
}
|
||||
|
||||
function isNewFormatPresetItem(item: unknown): item is SchedulingPresetItem {
|
||||
@@ -641,7 +672,7 @@ function loadFromConfig(cfg: PoolAdvancedConfig | null): PresetListItem[] {
|
||||
}
|
||||
|
||||
insertMissingByPreferredOrder(ordered, seen, defs, defsByName)
|
||||
return reorderDistributionGroup(ordered)
|
||||
return reorderDistributionGroup(normalizeMutexSelection(ordered))
|
||||
}
|
||||
|
||||
const legacyPresets = rawPresets as string[]
|
||||
@@ -664,33 +695,7 @@ function loadFromConfig(cfg: PoolAdvancedConfig | null): PresetListItem[] {
|
||||
}
|
||||
|
||||
insertMissingByPreferredOrder(ordered, seen, defs, defsByName)
|
||||
return reorderDistributionGroup(ordered)
|
||||
}
|
||||
|
||||
function normalizeMutexSelection(items: PresetListItem[]): PresetListItem[] {
|
||||
const next = [...items]
|
||||
const groups = new Map<string, number[]>()
|
||||
|
||||
next.forEach((item, index) => {
|
||||
if (!item.mutexGroup) return
|
||||
if (!groups.has(item.mutexGroup)) groups.set(item.mutexGroup, [])
|
||||
groups.get(item.mutexGroup)?.push(index)
|
||||
})
|
||||
|
||||
for (const indexes of groups.values()) {
|
||||
if (indexes.length <= 1) continue
|
||||
const enabledApplicable = indexes.find(index => {
|
||||
const item = next[index]
|
||||
return item.enabled && item.applicable
|
||||
})
|
||||
const firstApplicable = indexes.find(index => next[index].applicable)
|
||||
const winner = enabledApplicable ?? firstApplicable ?? indexes[0]
|
||||
indexes.forEach((index) => {
|
||||
next[index].enabled = index === winner && next[index].applicable
|
||||
})
|
||||
}
|
||||
|
||||
return next
|
||||
return reorderDistributionGroup(normalizeMutexSelection(ordered))
|
||||
}
|
||||
|
||||
function togglePreset(index: number, enabled: boolean) {
|
||||
@@ -814,15 +819,26 @@ function handleDrop(dropIndex: number) {
|
||||
dragOverIndex.value = null
|
||||
}
|
||||
|
||||
watch(() => props.modelValue, async (open) => {
|
||||
watch([() => props.modelValue, () => props.providerId], async ([open]) => {
|
||||
const revision = ++dialogRevision
|
||||
loading.value = false
|
||||
if (!open) return
|
||||
await ensurePresetDefsLoaded()
|
||||
if (!props.modelValue || dialogRevision !== revision) return
|
||||
presetList.value = normalizeMutexSelection(loadFromConfig(props.currentConfig))
|
||||
})
|
||||
|
||||
async function handleSave() {
|
||||
const providerId = props.providerId
|
||||
const revision = dialogRevision
|
||||
loading.value = true
|
||||
try {
|
||||
await ensurePresetDefsLoaded()
|
||||
if (!props.modelValue || props.providerId !== providerId || dialogRevision !== revision) return
|
||||
if (!presetDefsLoaded.value) {
|
||||
showError('调度策略元数据加载失败,请重试')
|
||||
return
|
||||
}
|
||||
presetList.value = normalizeMutexSelection(presetList.value)
|
||||
const schedulingPresets: SchedulingPresetItem[] = presetList.value.map(item => {
|
||||
const result: SchedulingPresetItem = {
|
||||
@@ -835,14 +851,17 @@ async function handleSave() {
|
||||
return result
|
||||
})
|
||||
|
||||
const mergedAdvanced: Record<string, unknown> = {
|
||||
...(props.currentConfig ?? {}),
|
||||
const latestProvider = await getProvider(providerId)
|
||||
if (!props.modelValue || props.providerId !== providerId || dialogRevision !== revision) return
|
||||
const latestAdvanced = (latestProvider as Record<string, unknown>).pool_advanced
|
||||
const mergedAdvanced = mergePoolAdvancedPatch(latestAdvanced, {
|
||||
scheduling_presets: schedulingPresets,
|
||||
}
|
||||
})
|
||||
const payload: Parameters<typeof updateProvider>[1] = {
|
||||
pool_advanced: mergedAdvanced as PoolAdvancedConfig,
|
||||
}
|
||||
const updatedProvider = await updateProvider(props.providerId, payload)
|
||||
const updatedProvider = await updateProvider(providerId, payload)
|
||||
if (!props.modelValue || props.providerId !== providerId || dialogRevision !== revision) return
|
||||
|
||||
success('号池调度已保存')
|
||||
emit('saved', updatedProvider)
|
||||
@@ -850,7 +869,9 @@ async function handleSave() {
|
||||
} catch (err) {
|
||||
showError(parseApiError(err))
|
||||
} finally {
|
||||
loading.value = false
|
||||
if (dialogRevision === revision) {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,22 +1,27 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { moveStrategyItem } from '@/features/pool/utils/poolSchedulingDialog'
|
||||
import {
|
||||
mergePoolAdvancedPatch,
|
||||
moveStrategyItem,
|
||||
normalizeMutexSelection,
|
||||
} from '@/features/pool/utils/poolSchedulingDialog'
|
||||
|
||||
interface TestPresetItem {
|
||||
preset: string
|
||||
mutexGroup: string | null
|
||||
enabled: boolean
|
||||
applicable: boolean
|
||||
}
|
||||
|
||||
function buildItems(): TestPresetItem[] {
|
||||
return [
|
||||
{ preset: 'cache_affinity', mutexGroup: 'distribution_mode', enabled: false },
|
||||
{ preset: 'lru', mutexGroup: 'distribution_mode', enabled: true },
|
||||
{ preset: 'single_account', mutexGroup: 'distribution_mode', enabled: false },
|
||||
{ preset: 'load_balance', mutexGroup: 'distribution_mode', enabled: false },
|
||||
{ preset: 'recent_refresh', mutexGroup: null, enabled: true },
|
||||
{ preset: 'quota_balanced', mutexGroup: null, enabled: false },
|
||||
{ preset: 'priority_first', mutexGroup: null, enabled: true },
|
||||
{ preset: 'cache_affinity', mutexGroup: 'distribution_mode', enabled: false, applicable: true },
|
||||
{ preset: 'lru', mutexGroup: 'distribution_mode', enabled: true, applicable: true },
|
||||
{ preset: 'single_account', mutexGroup: 'distribution_mode', enabled: false, applicable: true },
|
||||
{ preset: 'load_balance', mutexGroup: 'distribution_mode', enabled: false, applicable: true },
|
||||
{ preset: 'recent_refresh', mutexGroup: null, enabled: true, applicable: true },
|
||||
{ preset: 'quota_balanced', mutexGroup: null, enabled: false, applicable: true },
|
||||
{ preset: 'priority_first', mutexGroup: null, enabled: true, applicable: true },
|
||||
]
|
||||
}
|
||||
|
||||
@@ -69,4 +74,49 @@ describe('poolSchedulingDialog', () => {
|
||||
|
||||
expect(moved.map(item => item.preset)).toEqual(original.map(item => item.preset))
|
||||
})
|
||||
|
||||
it('keeps the first enabled distribution from the saved order', () => {
|
||||
const items = buildItems()
|
||||
items[0].enabled = true
|
||||
items[1].enabled = false
|
||||
items[3].enabled = true
|
||||
const savedOrder = [items[3], items[0], ...items.slice(1, 3), ...items.slice(4)]
|
||||
|
||||
const normalized = normalizeMutexSelection(savedOrder)
|
||||
|
||||
expect(normalized.find(item => item.preset === 'load_balance')?.enabled).toBe(true)
|
||||
expect(normalized.find(item => item.preset === 'cache_affinity')?.enabled).toBe(false)
|
||||
})
|
||||
|
||||
it('does not invent a distribution mode when all are disabled', () => {
|
||||
const items = buildItems().map(item => ({
|
||||
...item,
|
||||
enabled: item.mutexGroup ? false : item.enabled,
|
||||
}))
|
||||
|
||||
const normalized = normalizeMutexSelection(items)
|
||||
|
||||
expect(normalized.filter(item => item.mutexGroup).every(item => !item.enabled)).toBe(true)
|
||||
})
|
||||
|
||||
it('preserves pool fields that the current dialog does not edit', () => {
|
||||
const merged = mergePoolAdvancedPatch({
|
||||
sticky_session_ttl_seconds: 900,
|
||||
cost_window_seconds: 7200,
|
||||
cost_limit_per_key_tokens: 100_000,
|
||||
probing_target_percent: 25,
|
||||
global_priority: 7,
|
||||
}, {
|
||||
score_top_n: 256,
|
||||
})
|
||||
|
||||
expect(merged).toEqual({
|
||||
sticky_session_ttl_seconds: 900,
|
||||
cost_window_seconds: 7200,
|
||||
cost_limit_per_key_tokens: 100_000,
|
||||
probing_target_percent: 25,
|
||||
global_priority: 7,
|
||||
score_top_n: 256,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,6 +2,47 @@ export interface SchedulingDialogPresetLike {
|
||||
mutexGroup: string | null
|
||||
}
|
||||
|
||||
export interface SchedulingDialogSelectablePresetLike extends SchedulingDialogPresetLike {
|
||||
enabled: boolean
|
||||
applicable: boolean
|
||||
}
|
||||
|
||||
export function mergePoolAdvancedPatch(
|
||||
current: unknown,
|
||||
patch: Record<string, unknown>,
|
||||
): Record<string, unknown> {
|
||||
const currentRecord = typeof current === 'object' && current !== null && !Array.isArray(current)
|
||||
? current as Record<string, unknown>
|
||||
: {}
|
||||
return {
|
||||
...currentRecord,
|
||||
...patch,
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeMutexSelection<T extends SchedulingDialogSelectablePresetLike>(
|
||||
items: readonly T[],
|
||||
): T[] {
|
||||
const next = items.map(item => ({ ...item }))
|
||||
const groups = new Map<string, number[]>()
|
||||
|
||||
next.forEach((item, index) => {
|
||||
if (!item.mutexGroup) return
|
||||
const indexes = groups.get(item.mutexGroup) ?? []
|
||||
indexes.push(index)
|
||||
groups.set(item.mutexGroup, indexes)
|
||||
})
|
||||
|
||||
for (const indexes of groups.values()) {
|
||||
const winner = indexes.find(index => next[index].enabled && next[index].applicable)
|
||||
indexes.forEach((index) => {
|
||||
next[index].enabled = winner !== undefined && index === winner && next[index].applicable
|
||||
})
|
||||
}
|
||||
|
||||
return next
|
||||
}
|
||||
|
||||
export function moveStrategyItem<T extends SchedulingDialogPresetLike>(
|
||||
items: readonly T[],
|
||||
itemIndex: number,
|
||||
|
||||
Reference in New Issue
Block a user