using AxCopilot.Models; namespace AxCopilot.Services; /// /// 드래프트 큐 항목의 실행 전이와 재시도 정책을 담당합니다. /// ChatWindow가 직접 상태 전이를 조합하지 않도록 별도 서비스로 분리합니다. /// public sealed class DraftQueueProcessorService { public bool CanStartNext(ChatSessionStateService? session, string tab) => session?.GetNextQueuedDraft(tab) != null; public DraftQueueItem? TryStartNext(ChatSessionStateService? session, string tab, ChatStorageService? storage = null, string? preferredDraftId = null, TaskRunService? taskRuns = null) { if (session == null) return null; DraftQueueItem? next = null; if (!string.IsNullOrWhiteSpace(preferredDraftId)) { next = session.GetDraftQueueItems(tab) .FirstOrDefault(x => string.Equals(x.Id, preferredDraftId, StringComparison.OrdinalIgnoreCase)); if (next != null && !string.Equals(next.State, "queued", StringComparison.OrdinalIgnoreCase)) { session.ResetDraftToQueued(tab, next.Id, storage); next = session.GetDraftQueueItems(tab) .FirstOrDefault(x => string.Equals(x.Id, preferredDraftId, StringComparison.OrdinalIgnoreCase)); } } next ??= session.GetNextQueuedDraft(tab); if (next == null) return null; if (!session.MarkDraftRunning(tab, next.Id, storage)) return null; taskRuns?.StartQueueRun(tab, next.Id, next.Text); return session.GetDraftQueueItems(tab) .FirstOrDefault(x => string.Equals(x.Id, next.Id, StringComparison.OrdinalIgnoreCase)) ?? next; } public bool Complete(ChatSessionStateService? session, string tab, string draftId, ChatStorageService? storage = null, TaskRunService? taskRuns = null) { var completed = session?.MarkDraftCompleted(tab, draftId, storage) ?? false; if (completed) taskRuns?.CompleteQueueRun(tab, draftId, "대기열 작업 완료", "completed"); return completed; } public bool HandleFailure(ChatSessionStateService? session, string tab, string draftId, string? error, bool cancelled = false, int maxAutoRetries = 3, ChatStorageService? storage = null, TaskRunService? taskRuns = null) { if (session == null) return false; if (cancelled) { var reset = session.ResetDraftToQueued(tab, draftId, storage); if (reset) taskRuns?.CompleteQueueRun(tab, draftId, string.IsNullOrWhiteSpace(error) ? "대기열 작업 중단" : error, "cancelled"); return reset; } var handled = session.ScheduleDraftRetry(tab, draftId, error, maxAutoRetries, storage); if (handled) { var item = session.GetDraftQueueItems(tab) .FirstOrDefault(x => string.Equals(x.Id, draftId, StringComparison.OrdinalIgnoreCase)); var blocked = item?.NextRetryAt.HasValue == true; taskRuns?.CompleteQueueRun( tab, draftId, string.IsNullOrWhiteSpace(error) ? (blocked ? "재시도 대기" : "대기열 작업 실패") : error, blocked ? "blocked" : "failed"); } return handled; } public int PromoteReadyBlockedItems(ChatSessionStateService? session, string tab, ChatStorageService? storage = null) { if (session == null) return 0; var promoted = 0; foreach (var item in session.GetDraftQueueItems(tab) .Where(x => string.Equals(x.State, "queued", StringComparison.OrdinalIgnoreCase) && x.NextRetryAt.HasValue && x.NextRetryAt.Value <= DateTime.Now) .ToList()) { if (session.ResetDraftToQueued(tab, item.Id, storage)) promoted++; } return promoted; } public int ClearCompleted(ChatSessionStateService? session, string tab, ChatStorageService? storage = null) => ClearByState(session, tab, "completed", storage); public int ClearFailed(ChatSessionStateService? session, string tab, ChatStorageService? storage = null) => ClearByState(session, tab, "failed", storage); private static int ClearByState(ChatSessionStateService? session, string tab, string state, ChatStorageService? storage) { if (session == null) return 0; var removed = 0; foreach (var item in session.GetDraftQueueItems(tab) .Where(x => string.Equals(x.State, state, StringComparison.OrdinalIgnoreCase)) .ToList()) { if (session.RemoveDraft(tab, item.Id, storage)) removed++; } return removed; } }