base_agent/uas/inout/dispatcher.py

185 lines
7.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
"""
异步分发器
─────────────────────────────────────────────────────────────────
基于 QThreadPool + QRunnable 实现:
1. 接收 input_received 信号
2. 在工作线程中执行处理器链Pipeline
3. 处理完成后 emit output_ready 信号
线程间通信完全由 Qt 信号槽队列保证安全
─────────────────────────────────────────────────────────────────
"""
import logging
import traceback
from typing import List, Dict, Callable
from PyQt5.QtCore import QObject, QRunnable, QThreadPool, pyqtSlot, pyqtSignal
from uas.io.base_io import IOData, IOType
from uas.io.signals import io_signals
logger = logging.getLogger(__name__)
# ─────────────────────────────────────────────────────────────────
# Worker在线程池中执行处理器链
# ─────────────────────────────────────────────────────────────────
class _WorkerSignals(QObject):
"""Worker 内部信号QRunnable 不能直接定义 pyqtSignal"""
finished = pyqtSignal(object) # IOData
error = pyqtSignal(str, str) # data_id, error_msg
class _PipelineWorker(QRunnable):
"""
在 QThreadPool 工作线程中执行处理器链
每个 IOData 对应一个独立 Worker 实例
"""
def __init__(self, data: IOData, processors: List[Callable]):
super().__init__()
self.data = data
self.processors = processors
self.signals = _WorkerSignals()
self.setAutoDelete(True)
@pyqtSlot()
def run(self):
data = self.data
try:
data.mark_processing()
logger.debug(f"Worker 开始处理: {data}")
# 顺序执行处理器链
result = data.raw
for idx, processor in enumerate(self.processors):
io_signals.progress_updated.emit(
data.data_id,
int((idx / len(self.processors)) * 100)
)
result = processor(result, data)
data.mark_success(result)
io_signals.progress_updated.emit(data.data_id, 100)
logger.debug(f"Worker 处理完成: {data}")
self.signals.finished.emit(data)
except Exception as e:
msg = f"{type(e).__name__}: {e}\n{traceback.format_exc()}"
data.mark_error(msg)
logger.error(f"Worker 处理异常: {msg}")
self.signals.error.emit(data.data_id, msg)
# ─────────────────────────────────────────────────────────────────
# 分发器主类
# ─────────────────────────────────────────────────────────────────
class IODispatcher(QObject):
"""
IO 异步分发器
─────────────────────────────────────────────────────────────
使用方式:
dispatcher = IODispatcher()
dispatcher.register_processor(IOType.TEXT, clean_text)
dispatcher.register_processor(IOType.TEXT, translate_text)
dispatcher.start()
─────────────────────────────────────────────────────────────
"""
def __init__(self, max_threads: int = 8, parent=None):
super().__init__(parent)
self._pool: QThreadPool = QThreadPool.globalInstance()
self._pool.setMaxThreadCount(max_threads)
# { IOType: [processor_fn, ...] }
self._processors: Dict[str, List[Callable]] = {}
self._active = False
logger.info(f"IODispatcher 初始化,最大线程数={max_threads}")
# ── 处理器注册 ────────────────────────────────────────────────
def register_processor(
self,
io_type: IOType,
processor: Callable,
index: int = -1,
) -> None:
"""
注册处理器到指定 IO 类型的处理链
:param io_type: 目标类型IOType.TEXT 等)
:param processor: fn(raw, IOData) -> processed_result
:param index: 插入位置,-1 表示追加到末尾
"""
key = io_type.value
if key not in self._processors:
self._processors[key] = []
if index == -1:
self._processors[key].append(processor)
else:
self._processors[key].insert(index, processor)
logger.info(f"注册处理器: {processor.__name__} -> {key}")
def unregister_processor(self, io_type: IOType, processor: Callable) -> None:
key = io_type.value
if key in self._processors:
self._processors[key] = [
p for p in self._processors[key] if p is not processor
]
def clear_processors(self, io_type: IOType) -> None:
self._processors.pop(io_type.value, None)
# ── 生命周期 ──────────────────────────────────────────────────
def start(self) -> None:
"""启动分发器,连接输入信号"""
if self._active:
return
io_signals.input_received.connect(self._on_input_received)
self._active = True
logger.info("✅ IODispatcher 已启动")
def stop(self) -> None:
"""停止分发器"""
if not self._active:
return
io_signals.input_received.disconnect(self._on_input_received)
self._pool.waitForDone(3000)
self._active = False
logger.info("🛑 IODispatcher 已停止")
# ── 核心槽函数 ────────────────────────────────────────────────
@pyqtSlot(str, str, object)
def _on_input_received(self, data_id: str, io_type: str, data: object):
"""
槽:接收原始输入,提交到线程池处理
Qt 信号槽队列保证此函数在主线程执行,
实际处理在 QRunnable 工作线程中进行
"""
processors = self._processors.get(io_type, [])
if not processors:
# 无处理器,直接透传
logger.debug(f"无处理器,透传 data_id={data_id[:8]} type={io_type}")
data.mark_success()
io_signals.output_ready.emit(data_id, io_type, data)
return
worker = _PipelineWorker(data, processors)
worker.signals.finished.connect(self._on_worker_finished)
worker.signals.error.connect(self._on_worker_error)
self._pool.start(worker)
logger.debug(f"提交 Worker: data_id={data_id[:8]} type={io_type} "
f"processors={len(processors)}")
@pyqtSlot(object)
def _on_worker_finished(self, data: object):
"""Worker 完成,发射 output_ready"""
io_signals.output_ready.emit(
data.data_id, data.io_type.value, data
)
@pyqtSlot(str, str)
def _on_worker_error(self, data_id: str, error_msg: str):
logger.error(f"Worker 错误 data_id={data_id[:8]}: {error_msg}")