144 lines
4.6 KiB
Swift
144 lines
4.6 KiB
Swift
import Foundation
|
||
import SwiftUI
|
||
|
||
// MARK: - 任务状态轮询器
|
||
// 负责定时轮询任务状态,并在状态变化时通知观察者
|
||
|
||
@MainActor
|
||
class TaskStatusPoller: ObservableObject {
|
||
// MARK: - Published Properties
|
||
|
||
@Published var status: TaskStatus = .pending
|
||
@Published var progress: Int = 0
|
||
@Published var currentStep: String?
|
||
@Published var errorMessage: String?
|
||
@Published var courseId: String?
|
||
@Published var taskCreatedAt: String? // ✅ 新增:任务创建时间(ISO 8601格式)
|
||
|
||
// MARK: - Private Properties
|
||
|
||
private let taskId: String
|
||
private var timer: Timer?
|
||
private let pollInterval: TimeInterval = 2.0 // 2秒轮询一次
|
||
private let maxPollTime: TimeInterval = 900.0 // 最多轮询15分钟
|
||
private var startTime: Date?
|
||
private var isPolling: Bool = false
|
||
|
||
// MARK: - Callbacks
|
||
|
||
var onStatusChanged: ((TaskStatus) -> Void)?
|
||
var onCompleted: ((String) -> Void)? // courseId
|
||
var onFailed: ((String) -> Void)? // errorMessage
|
||
var onTimeout: (() -> Void)?
|
||
|
||
// MARK: - Initialization
|
||
|
||
init(taskId: String) {
|
||
self.taskId = taskId
|
||
}
|
||
|
||
// MARK: - Public Methods
|
||
|
||
/// 开始轮询
|
||
func start() {
|
||
guard !isPolling else {
|
||
print("⚠️ [TaskStatusPoller] 已经在轮询中,跳过")
|
||
return
|
||
}
|
||
|
||
print("🔵 [TaskStatusPoller] 开始轮询: taskId=\(taskId)")
|
||
isPolling = true
|
||
startTime = Date()
|
||
|
||
// 立即执行一次(异步,不阻塞主线程)
|
||
Task { @MainActor in
|
||
print("🔵 [TaskStatusPoller] 立即执行第一次轮询")
|
||
await poll()
|
||
}
|
||
|
||
// 设置定时器
|
||
timer = Timer.scheduledTimer(withTimeInterval: pollInterval, repeats: true) { [weak self] _ in
|
||
Task { @MainActor [weak self] in
|
||
await self?.poll()
|
||
}
|
||
}
|
||
|
||
print("✅ [TaskStatusPoller] 轮询器已启动,定时器已设置")
|
||
}
|
||
|
||
/// 停止轮询
|
||
func stop() {
|
||
isPolling = false
|
||
timer?.invalidate()
|
||
timer = nil
|
||
}
|
||
|
||
// MARK: - Private Methods
|
||
|
||
private func poll() async {
|
||
guard isPolling else {
|
||
print("⚠️ [TaskStatusPoller] 轮询已停止,跳过")
|
||
return
|
||
}
|
||
|
||
print("🔵 [TaskStatusPoller] 开始轮询: taskId=\(taskId)")
|
||
|
||
// 检查超时
|
||
if let startTime = startTime,
|
||
Date().timeIntervalSince(startTime) > maxPollTime {
|
||
print("⏰ [TaskStatusPoller] 轮询超时")
|
||
stop()
|
||
onTimeout?()
|
||
return
|
||
}
|
||
|
||
do {
|
||
print("🔵 [TaskStatusPoller] 调用 getTaskStatus API...")
|
||
let statusData = try await AICourseService.shared.getTaskStatus(taskId: taskId)
|
||
print("✅ [TaskStatusPoller] 获取状态成功: status=\(statusData.status), progress=\(statusData.progress)")
|
||
|
||
// 更新状态
|
||
let newStatus = TaskStatus(rawValue: statusData.status) ?? .pending
|
||
|
||
if newStatus != status {
|
||
print("🔄 [TaskStatusPoller] 状态变化: \(status) -> \(newStatus)")
|
||
status = newStatus
|
||
onStatusChanged?(newStatus)
|
||
}
|
||
|
||
progress = statusData.progress
|
||
currentStep = statusData.currentStep
|
||
errorMessage = statusData.errorMessage
|
||
courseId = statusData.courseId
|
||
taskCreatedAt = statusData.createdAt // ✅ 新增:保存任务创建时间
|
||
|
||
// 检查完成状态
|
||
if newStatus.isCompleted {
|
||
print("✅ [TaskStatusPoller] 任务完成")
|
||
stop()
|
||
// courseId 是必需的,直接使用
|
||
onCompleted?(statusData.courseId)
|
||
} else if newStatus.isFailed {
|
||
print("❌ [TaskStatusPoller] 任务失败")
|
||
stop()
|
||
onFailed?(statusData.errorMessage ?? "生成失败")
|
||
}
|
||
|
||
} catch {
|
||
// 轮询失败不停止,继续尝试
|
||
print("❌ [TaskStatusPoller] 轮询失败: \(error.localizedDescription)")
|
||
print("❌ [TaskStatusPoller] 错误类型: \(type(of: error))")
|
||
}
|
||
}
|
||
|
||
deinit {
|
||
// 只捕获 timer 引用并在 MainActor 上失效,避免闭包捕获 self 导致 Swift 6 报错
|
||
let t = timer
|
||
timer = nil
|
||
isPolling = false
|
||
Task { @MainActor in
|
||
t?.invalidate()
|
||
}
|
||
}
|
||
}
|