base_agent/uas/task/task_executor.py

860 lines
35 KiB
Python
Raw Normal View History

2026-06-18 03:28:14 +00:00
# -*- 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):
"""分发单个 ActionEntryCONTINUOUS/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()