# -*- coding: utf-8 -*- """ Task 执行器 ───────────────────────────────────────────────────────────────── 基于 PyQt5 QObject/pyqtSignal + QThreadPool 实现: 核心调度逻辑: 1. 任务启动时,按时间轴扫描航线各段 2. 进入航段时: a. 并行组 → 同时提交所有 Action 到 ActionExecutor b. 串行组 → 按 order 顺序依次提交,等待上一个完成再提交下一个 c. 串/并行组的执行顺序由 serial_first 控制 3. 航线级 Action 在整条航线 ON_ENTER/ON_EXIT 时触发 4. CONTINUOUS 类型 Action 由 QTimer 周期触发 5. SCHEDULED 类型 Action 由 QTimer.singleShot 在指定偏移时刻触发 ───────────────────────────────────────────────────────────────── """ from __future__ import annotations import functools import logging import threading import time from typing import Dict, List, Optional from PyQt5.QtCore import ( QObject, QRunnable, QThreadPool, pyqtSignal, pyqtSlot, QTimer ) from uas.event.event_handler import on_event from uas.event.event_model import Event from uas.event.event_type import EventType from uas.task.task_action_plan import ( ActionEntry, SegmentActionPlan, RouteActionPlan, TriggerMoment ) from uas.task.task_model import Task, TaskState logger = logging.getLogger(__name__) # ───────────────────────────────────────────────────────────────── # 串行 Action 链执行器(在工作线程中顺序执行) # ───────────────────────────────────────────────────────────────── class _SerialChainSignals(QObject): finished = pyqtSignal(str, list) # task_id, [ActionResult] error = pyqtSignal(str, str) # task_id, error_msg class _SerialChainWorker(QRunnable): """ 在 QThreadPool 工作线程中串行执行一组 ActionEntry 每个 Action 完成后才执行下一个(严格顺序保证) """ def __init__( self, task_id: str, entries: List[ActionEntry], executor, # ActionExecutor 引用 label: str = "", # 调试标签(如 "route" / "seg[0]") ): super().__init__() self.task_id = task_id self.entries = entries # 已按 order 排序 self.executor = executor self.label = label self.signals = _SerialChainSignals() self.setAutoDelete(True) @pyqtSlot() def run(self): results = [] for entry in self.entries: if not entry.enabled: continue try: logger.debug( f"[串行/{self.label}] 执行 Action: " f"{entry.action.type_key} order={entry.order}" ) # submit_sync 阻塞当前工作线程,保证严格串行 result = self.executor.submit_sync(entry.action) results.append(result) logger.debug( f"[串行/{self.label}] 完成 Action: " f"{entry.action.type_key} success={result.success}" ) except Exception as e: logger.error( f"[串行/{self.label}] Action 执行异常: " f"{entry.action.type_key} → {e}", exc_info=True, ) self.signals.error.emit(self.task_id, str(e)) return # 串行链中断 self.signals.finished.emit(self.task_id, results) # ───────────────────────────────────────────────────────────────── # 航段调度器(管理单个航段的 Action 触发) # ───────────────────────────────────────────────────────────────── class _SegmentScheduler(QObject): """ 管理单个航段的 Action 调度 负责:ON_ENTER 触发、CONTINUOUS 定时器、SCHEDULED 单次定时器、ON_EXIT 触发 继承 QObject 以便在内部安全使用 QTimer """ serial_done = pyqtSignal(str, int, list) # task_id, seg_index, results serial_error = pyqtSignal(str, int, str) # task_id, seg_index, error def __init__( self, task: Task, seg_index: int, plan: SegmentActionPlan, action_executor, thread_pool: QThreadPool, parent: QObject = None, ): super().__init__(parent) self.task = task self.seg_index = seg_index self.plan = plan self.action_executor = action_executor self.pool = thread_pool self._timers: List[QTimer] = [] self._active = False # ── 进入航段 ────────────────────────────────────────────────── def start(self): """进入航段时调用:触发 ON_ENTER + 启动定时器""" self._active = True logger.info( f"[Seg{self.seg_index}] 进入,启动 Action 调度 " f"task={self.task.task_id[:8]}" ) self.task.add_record( "action_start", f"进入航段[{self.seg_index}],开始 Action 调度", ) # ON_ENTER 触发 on_enter = self.plan.entries_by_trigger(TriggerMoment.ON_ENTER) if on_enter: self._dispatch_entries(on_enter, label=f"seg{self.seg_index}/enter") # SCHEDULED 触发(单次定时器) for entry in self.plan.entries_by_trigger(TriggerMoment.SCHEDULED): delay_ms = max(0, int(entry.time_offset_s * 1000)) t = QTimer(self) t.setSingleShot(True) t.timeout.connect(lambda e=entry: self._dispatch_single(e)) t.start(delay_ms) self._timers.append(t) logger.debug( f"[Seg{self.seg_index}] SCHEDULED Action " f"{entry.action.type_key} 将在 {delay_ms}ms 后触发" ) # CONTINUOUS 触发(周期定时器) for entry in self.plan.entries_by_trigger(TriggerMoment.CONTINUOUS): interval_ms = max(100, int(entry.interval_s * 1000)) t = QTimer(self) t.timeout.connect(lambda e=entry: self._dispatch_single(e)) t.start(interval_ms) self._timers.append(t) logger.debug( f"[Seg{self.seg_index}] CONTINUOUS Action " f"{entry.action.type_key} 每 {interval_ms}ms 触发" ) # ── 离开航段 ────────────────────────────────────────────────── def stop(self): """离开航段时调用:停止所有定时器 + 触发 ON_EXIT""" if not self._active: return self._active = False # 停止并销毁所有定时器 for t in self._timers: t.stop() t.deleteLater() self._timers.clear() # ON_EXIT 触发 on_exit = self.plan.entries_by_trigger(TriggerMoment.ON_EXIT) if on_exit: self._dispatch_entries(on_exit, label=f"seg{self.seg_index}/exit") self.task.add_record( "action_start", f"离开航段[{self.seg_index}],ON_EXIT Actions 已触发", ) logger.info(f"[Seg{self.seg_index}] 离开,定时器已清理") # ── 核心分发逻辑 ────────────────────────────────────────────── def _dispatch_entries(self, entries: List[ActionEntry], label: str = ""): """ 按策略分发一组 ActionEntry: ┌─────────────────────────────────────────────────────┐ │ 并行组(order=None)→ 同时提交到 ActionExecutor │ │ 串行组(order!=None)→ 提交到 SerialChainWorker │ │ serial_first 控制两组相对启动顺序 │ └─────────────────────────────────────────────────────┘ 注意:串行组提交到线程池后立即返回(非阻塞), 线程池内部保证串行顺序,不阻塞主线程。 """ parallel_entries = [e for e in entries if e.is_parallel and e.enabled] sequential_entries = sorted( [e for e in entries if e.is_sequential and e.enabled], key=lambda e: e.order, ) def _run_parallel(): for entry in parallel_entries: self.action_executor.submit(entry.action) logger.debug( f"[并行/{label}] 提交 Action: {entry.action.type_key}" ) def _run_sequential(): if not sequential_entries: return worker = _SerialChainWorker( task_id = self.task.task_id, entries = sequential_entries, executor = self.action_executor, label = label, ) worker.signals.finished.connect( lambda tid, res, si=self.seg_index: self.serial_done.emit(tid, si, res) ) worker.signals.error.connect( lambda tid, err, si=self.seg_index: self.serial_error.emit(tid, si, err) ) self.pool.start(worker) logger.debug( f"[串行/{label}] 提交串行链,共 {len(sequential_entries)} 个 Action" ) # 根据 serial_first 决定启动顺序 if self.plan.serial_first: _run_sequential() _run_parallel() else: _run_parallel() _run_sequential() def _dispatch_single(self, entry: ActionEntry): """分发单个 ActionEntry(CONTINUOUS/SCHEDULED 回调)""" if entry.enabled and self._active: self.action_executor.submit(entry.action) logger.debug( f"[Seg{self.seg_index}] 触发 Action: {entry.action.type_key} " f"trigger={entry.trigger.value}" ) # ───────────────────────────────────────────────────────────────── # 航线级 Action 调度器 # ───────────────────────────────────────────────────────────────── class _RouteScheduler(QObject): """ 管理整条航线级别的 Action 调度 负责:ON_ENTER/ON_EXIT/CONTINUOUS/SCHEDULED 触发 """ serial_done = pyqtSignal(str, list) # task_id, results serial_error = pyqtSignal(str, str) # task_id, error def __init__( self, task: Task, action_executor, thread_pool: QThreadPool, parent: QObject = None, ): super().__init__(parent) self.task = task self.action_executor = action_executor self.pool = thread_pool self._timers: List[QTimer] = [] self._active = False def start(self): """航线开始时调用""" self._active = True plan = self.task.action_plan # ON_ENTER 触发 on_enter = plan.route_entries_by_trigger(TriggerMoment.ON_ENTER) if on_enter: self._dispatch_entries(on_enter, plan) # SCHEDULED 触发 for entry in plan.route_entries_by_trigger(TriggerMoment.SCHEDULED): delay_ms = max(0, int(entry.time_offset_s * 1000)) t = QTimer(self) t.setSingleShot(True) t.timeout.connect(lambda e=entry: self._dispatch_single(e)) t.start(delay_ms) self._timers.append(t) # CONTINUOUS 触发 for entry in plan.route_entries_by_trigger(TriggerMoment.CONTINUOUS): interval_ms = max(100, int(entry.interval_s * 1000)) t = QTimer(self) t.timeout.connect(lambda e=entry: self._dispatch_single(e)) t.start(interval_ms) self._timers.append(t) logger.info( f"[FlightRoute] 航线级调度启动 task={self.task.task_id[:8]} " f"entries={len(plan.route_entries)}" ) def stop(self): """航线结束时调用""" if not self._active: return self._active = False for t in self._timers: t.stop() t.deleteLater() self._timers.clear() # ON_EXIT 触发 plan = self.task.action_plan on_exit = plan.route_entries_by_trigger(TriggerMoment.ON_EXIT) if on_exit: self._dispatch_entries(on_exit, plan) logger.info(f"[FlightRoute] 航线级调度停止 task={self.task.task_id[:8]}") def _dispatch_entries(self, entries: List[ActionEntry], plan: RouteActionPlan): parallel_entries = [e for e in entries if e.is_parallel and e.enabled] sequential_entries = sorted( [e for e in entries if e.is_sequential and e.enabled], key=lambda e: e.order, ) def _run_parallel(): for entry in parallel_entries: self.action_executor.submit(entry.action) logger.debug(f"[并行/route] 提交: {entry.action.type_key}") def _run_sequential(): if not sequential_entries: return worker = _SerialChainWorker( task_id = self.task.task_id, entries = sequential_entries, executor = self.action_executor, label = "route", ) worker.signals.finished.connect( lambda tid, res: self.serial_done.emit(tid, res) ) worker.signals.error.connect( lambda tid, err: self.serial_error.emit(tid, err) ) self.pool.start(worker) if plan.serial_first: _run_sequential() _run_parallel() else: _run_parallel() _run_sequential() def _dispatch_single(self, entry: ActionEntry): if entry.enabled and self._active: self.action_executor.submit(entry.action) # ───────────────────────────────────────────────────────────────── # Task 执行器主类 # ───────────────────────────────────────────────────────────────── class TaskExecutor(QObject): """ Task 执行器(单例) ───────────────────────────────────────────────────────────── 使用方式: executor = TaskExecutor.instance() executor.set_action_executor(action_executor) executor.submit(task) ───────────────────────────────────────────────────────────── """ _instance = None _lock = threading.Lock() # ── 对外信号 ────────────────────────────────────────────────── task_submitted = pyqtSignal(object) # Task task_started = pyqtSignal(object) # Task task_completed = pyqtSignal(object) # Task task_failed = pyqtSignal(object, str) # Task, error task_cancelled = pyqtSignal(object) # Task task_paused = pyqtSignal(object) # Task task_resumed = pyqtSignal(object) # Task segment_entered = pyqtSignal(object, int) # Task, seg_index segment_exited = pyqtSignal(object, int) # Task, seg_index action_triggered = pyqtSignal(object, object) # Task, ActionEntry @classmethod def instance(cls) -> "TaskExecutor": with cls._lock: if cls._instance is None: cls._instance = cls() return cls._instance def __init__(self, max_threads: int = 8, parent=None): super().__init__(parent) self._pool = QThreadPool.globalInstance() self._pool.setMaxThreadCount(max_threads) self._action_executor = None # 运行中的任务 { task_id: Task } self._running: Dict[str, Task] = {} self._run_lock = threading.Lock() # 航线推进定时器 { task_id: QTimer } self._route_timers: Dict[str, QTimer] = {} # 航段调度器列表 { task_id: List[_SegmentScheduler | None] } self._seg_schedulers: Dict[str, List] = {} # 航线级调度器 { task_id: _RouteScheduler } self._route_schedulers: Dict[str, _RouteScheduler] = {} # # 当前活跃航段索引 { task_id: int } # self._seg_cursor: Dict[str, int] = {} # # 航段时间边界缓存 { task_id: [(t_start, t_end), ...] } # self._seg_times: Dict[str, List[Tuple[float, float]]] = {} # 任务实际启动时刻(用于时间轴对齐){ task_id: float } # self._task_wall_start: Dict[str, float] = {} logger.info(f"✅ TaskExecutor 初始化 max_threads={max_threads}") @on_event(EventType.ROUTE_SEGMENT_EXITED,EventType.ROUTE_SEGMENT_ENTERED,EventType.ROUTE_WAYPOINT_REACHED) def handle_route_event(self, event: Event): segment_index = event.params.get("segment_index") task = event.params.get("task") self.start_segment_actions(event.event_type, segment_index, task) def set_action_executor(self, executor) -> None: """注入 ActionExecutor""" self._action_executor = executor logger.info("✅ ActionExecutor 已注入 TaskExecutor") # ── 提交接口 ────────────────────────────────────────────────── def submit(self, task: Task) -> str: """ 异步提交任务(立即返回) :return: task_id """ if not self._action_executor: raise RuntimeError("请先调用 set_action_executor() 注入 ActionExecutor") task.mark_ready() self.task_submitted.emit(task) with self._run_lock: self._running[task.task_id] = task _task_procedure = functools.partial(self._start_task, task) # 若有计划执行时间,延迟启动 if task.scheduled_at and task.scheduled_at > time.time(): delay_ms = int((task.scheduled_at - time.time()) * 1000) QTimer.singleShot(delay_ms, _task_procedure) logger.info(f"任务已调度,{delay_ms}ms 后启动: {task.name}") else: # 使用 singleShot(0) 确保在 Qt 事件循环中启动,避免递归 QTimer.singleShot(0, _task_procedure) return task.task_id def cancel(self, task_id: str) -> bool: """取消任务""" with self._run_lock: task = self._running.get(task_id) if task: task.request_cancel() self._cleanup_task(task_id) task.mark_cancelled() self.task_cancelled.emit(task) logger.info(f"🚫 任务已取消: {task.name}") return True return False def pause(self, task_id: str) -> bool: """暂停任务(暂停航线推进定时器)""" timer = self._route_timers.get(task_id) task = self._running.get(task_id) if timer and task and task.is_running: timer.stop() task.mark_paused() self.task_paused.emit(task) logger.info(f"⏸ 任务已暂停: {task.name}") return True return False def resume(self, task_id: str) -> bool: """恢复任务""" timer = self._route_timers.get(task_id) task = self._running.get(task_id) if timer and task and task.state == TaskState.PAUSED: timer.start(100) task.mark_resumed() self.task_resumed.emit(task) logger.info(f"▶ 任务已恢复: {task.name}") return True return False # ── 内部:任务启动 ──────────────────────────────────────────── @pyqtSlot() def _start_task(self, task: Task): """在 Qt 事件循环中启动任务""" if task.cancel_requested: return task.mark_running() # self._task_wall_start[task.task_id] = time.time() self.task_started.emit(task) logger.info(f"▶ 任务开始执行: {task.name} [{task.task_id[:8]}]") # ── 启动航线级调度器 ────────────────────────────────────── route_sched = _RouteScheduler( task = task, action_executor = self._action_executor, thread_pool = self._pool, parent = self, ) route_sched.serial_done.connect(self._on_route_serial_done) route_sched.serial_error.connect(self._on_route_serial_error) self._route_schedulers[task.task_id] = route_sched route_sched.start() # ── 无航线任务:直接完成 ────────────────────────────────── if not task.has_route: self._execute_no_route_task(task) return # ── 缓存各航段时间边界(相对于任务启动时刻的偏移)──────── # seg_times: List[Tuple[float, float]] = [] # for seg in task.route.segments: # # 航段时间 = 路由内部时间轴(相对于 route.start_time) # # 转换为相对于任务实际启动时刻的偏移 # offset = self._task_wall_start[task.task_id] - task.route.start_time # seg_times.append((seg.t_start + offset, seg.t_end + offset)) # self._seg_times[task.task_id] = seg_times # ── 初始化各航段调度器 ──────────────────────────────────── schedulers: List[Optional[_SegmentScheduler]] = [] for idx in range(len(task.route.segments)): plan = task.action_plan.segment_plans.get(idx) if plan: sched = _SegmentScheduler( task = task, seg_index = idx, plan = plan, action_executor = self._action_executor, thread_pool = self._pool, parent = self, ) sched.serial_done.connect(self._on_seg_serial_done) sched.serial_error.connect(self._on_seg_serial_error) else: sched = None schedulers.append(sched) self._seg_schedulers[task.task_id] = schedulers # self._seg_cursor[task.task_id] = -1 # -1 表示尚未进入任何航段 # ── 启动航线推进定时器(100ms 轮询时间轴)──────────────── # timer = QTimer(self) # _task_route = functools.partial(self._advance_route, task) # timer.timeout.connect(_task_route) # timer.start(100) # self._route_timers[task.task_id] = timer def _execute_no_route_task(self, task: Task): """ 执行无航线任务: 直接调度所有 route_entries(并行 + 串行),完成后结束任务 """ plan = task.action_plan # 并行组 for entry in plan.parallel_route_entries: if entry.enabled: self._action_executor.submit(entry.action) # 串行组 seq_entries = plan.sequential_route_entries if seq_entries: worker = _SerialChainWorker( task_id = task.task_id, entries = seq_entries, executor = self._action_executor, label = "no_route", ) worker.signals.finished.connect( lambda tid, res: self._finish_task(task) ) worker.signals.error.connect( lambda tid, err: self._fail_task(task, err) ) self._pool.start(worker) else: # 无串行组,等待并行 Action 提交后直接完成 QTimer.singleShot(500, lambda: self._finish_task(task)) # ── 内部:航线推进(时间轴轮询)────────────────────────────── @pyqtSlot() def start_segment_actions(self, event_type: EventType, seg_index: int, task: Task): if task.cancel_requested or not task.is_running: self._cleanup_task(task.task_id) if task.cancel_requested: task.mark_cancelled() self.task_cancelled.emit(task) return # now = time.time() # seg_times = self._seg_times.get(task.task_id, []) schedulers = self._seg_schedulers.get(task.task_id, []) # cur_idx = self._seg_cursor.get(task.task_id, -1) # n_segs = len(seg_times) # # if not seg_times: # return # ## ── 判断当前时刻落在哪个航段 ────────────────────────────── # new_idx = cur_idx # for i, (t_start, t_end) in enumerate(seg_times): # if t_start <= now <= t_end: # new_idx = i # break # else: # # 超出所有航段时间范围 # if now > seg_times[-1][1]: # new_idx = n_segs # 标记为"已完成所有航段" match event_type: case EventType.ROUTE_SEGMENT_ENTERED: if schedulers[seg_index]: schedulers[seg_index].start() self.segment_entered.emit(task, seg_index) logger.info( f"[FlightRoute] 进入航段[{seg_index}] task={task.task_id[:8]}" ) case EventType.ROUTE_SEGMENT_EXITED: if schedulers[seg_index]: schedulers[seg_index].stop() self.segment_exited.emit(task, seg_index) logger.info( f"[FlightRoute] 离开航段[{seg_index}] task={task.task_id[:8]}" ) case EventType.ROUTE_WAYPOINT_REACHED: pass if seg_index == len(task.route.segments): self._finish_task(task) # # ── 航段切换处理 ────────────────────────────────────────── # if new_idx != cur_idx: # # 离开旧航段 # if 0 <= cur_idx < len(schedulers) and schedulers[cur_idx]: # schedulers[cur_idx].stop() # self.segment_exited.emit(task, cur_idx) # logger.info( # f"[FlightRoute] 离开航段[{cur_idx}] task={task.task_id[:8]}" # ) # # # 进入新航段 # if 0 <= new_idx < len(schedulers): # if schedulers[new_idx]: # schedulers[new_idx].start() # self.segment_entered.emit(task, new_idx) # logger.info( # f"[FlightRoute] 进入航段[{new_idx}] task={task.task_id[:8]}" # ) # self._seg_cursor[task.task_id] = new_idx # # # 所有航段执行完毕 # elif new_idx >= n_segs: # self._seg_cursor[task.task_id] = new_idx # self._finish_task(task) @pyqtSlot() def _advance_route(self, task: Task): """ 每 100ms 轮询一次: 根据当前真实时刻与各航段时间边界,判断是否需要进入/离开航段 """ if task.cancel_requested or not task.is_running: self._cleanup_task(task.task_id) if task.cancel_requested: task.mark_cancelled() self.task_cancelled.emit(task) return now = time.time() seg_times = self._seg_times.get(task.task_id, []) schedulers = self._seg_schedulers.get(task.task_id, []) cur_idx = self._seg_cursor.get(task.task_id, -1) n_segs = len(seg_times) if not seg_times: return # ── 判断当前时刻落在哪个航段 ────────────────────────────── new_idx = cur_idx for i, (t_start, t_end) in enumerate(seg_times): if t_start <= now <= t_end: new_idx = i break else: # 超出所有航段时间范围 if now > seg_times[-1][1]: new_idx = n_segs # 标记为"已完成所有航段" # ── 航段切换处理 ────────────────────────────────────────── if new_idx != cur_idx: # 离开旧航段 if 0 <= cur_idx < len(schedulers) and schedulers[cur_idx]: schedulers[cur_idx].stop() self.segment_exited.emit(task, cur_idx) logger.info( f"[FlightRoute] 离开航段[{cur_idx}] task={task.task_id[:8]}" ) # 进入新航段 if 0 <= new_idx < len(schedulers): if schedulers[new_idx]: schedulers[new_idx].start() self.segment_entered.emit(task, new_idx) logger.info( f"[FlightRoute] 进入航段[{new_idx}] task={task.task_id[:8]}" ) self._seg_cursor[task.task_id] = new_idx # 所有航段执行完毕 elif new_idx >= n_segs: self._seg_cursor[task.task_id] = new_idx self._finish_task(task) # ── 内部:任务完成/失败 ─────────────────────────────────────── def _finish_task(self, task: Task): """正常完成任务""" if task.is_done: return self._cleanup_task(task.task_id) task.mark_completed() self.task_completed.emit(task) logger.info(f"✅ 任务完成: {task.name} 耗时={task.elapsed_s:.1f}s") def _fail_task(self, task: Task, reason: str): """任务失败""" if task.is_done: return self._cleanup_task(task.task_id) task.mark_failed(reason) self.task_failed.emit(task, reason) logger.error(f"❌ 任务失败: {task.name} reason={reason}") def _cleanup_task(self, task_id: str): """清理任务相关资源""" # 停止航线推进定时器 timer = self._route_timers.pop(task_id, None) if timer: timer.stop() timer.deleteLater() # 停止所有航段调度器 for sched in self._seg_schedulers.pop(task_id, []): if sched: sched.stop() # 停止航线级调度器 route_sched = self._route_schedulers.pop(task_id, None) if route_sched: route_sched.stop() # 清理游标和时间缓存 self._seg_cursor.pop(task_id, None) # self._seg_times.pop(task_id, None) # self._task_wall_start.pop(task_id, None) with self._run_lock: self._running.pop(task_id, None) # ── 槽函数 ──────────────────────────────────────────────────── @pyqtSlot(str, int, list) def _on_seg_serial_done(self, task_id: str, seg_index: int, results: list): logger.info( f"[串行链] 航段[{seg_index}] 完成 " f"results={len(results)} task={task_id[:8]}" ) task = self._running.get(task_id) if task: task.add_record( "action_done", f"航段[{seg_index}] 串行 Action 链完成,共 {len(results)} 个", {"seg_index": seg_index, "results_count": len(results)}, ) @pyqtSlot(str, int, str) def _on_seg_serial_error(self, task_id: str, seg_index: int, error: str): logger.error(f"[串行链] 航段[{seg_index}] 错误: {error}") task = self._running.get(task_id) if task: task.add_record("error", f"航段[{seg_index}] 串行链错误: {error}") @pyqtSlot(str, list) def _on_route_serial_done(self, task_id: str, results: list): logger.info( f"[串行链] 航线级完成 results={len(results)} task={task_id[:8]}" ) @pyqtSlot(str, str) def _on_route_serial_error(self, task_id: str, error: str): logger.error(f"[串行链] 航线级错误: {error}") # ── 查询接口 ────────────────────────────────────────────────── def get_running_tasks(self) -> List[Task]: with self._run_lock: return list(self._running.values()) def get_task(self, task_id: str) -> Optional[Task]: with self._run_lock: return self._running.get(task_id) # 全局执行器实例 task_executor = TaskExecutor.instance()