2026-06-16 17:53:35 +08:00
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
"""
|
2026-06-16 18:23:38 +08:00
|
|
|
|
MainView —— 端到端模块化新架构的路由与调度壳(主窗口)
|
|
|
|
|
|
=========================================================
|
2026-06-16 17:53:35 +08:00
|
|
|
|
|
|
|
|
|
|
职责
|
|
|
|
|
|
----
|
|
|
|
|
|
1. **路由**:根据 ``ROUTES`` 表动态加载每个 step 的 view(前端皮囊)
|
|
|
|
|
|
和 service(后端大脑),左侧 ``QListWidget`` 导航,右侧
|
|
|
|
|
|
``QStackedWidget`` 承载 view。
|
2026-06-16 18:23:38 +08:00
|
|
|
|
2. **解耦通讯**:view 不直接持有 main_view 引用;按钮点击通过
|
2026-06-16 17:53:35 +08:00
|
|
|
|
``BaseView.dispatch_execute`` 沿父链回到本类 ``run_single_step``。
|
2026-06-16 18:23:38 +08:00
|
|
|
|
3. **后台执行**:内置 ``TaskWorker(QThread)``(三信号协议:
|
|
|
|
|
|
``log_msg / finished / error``),把 service 调用丢进子线程,
|
|
|
|
|
|
避免阻塞 UI。
|
2026-06-16 17:53:35 +08:00
|
|
|
|
4. **日志** :底部 ``QTextEdit`` 集中输出路由与 service 结果。
|
|
|
|
|
|
|
2026-06-16 18:23:38 +08:00
|
|
|
|
本文件由原 ``src/new/main_router.py`` 迁移而来,类名 ``MainRouter`` 改名为
|
|
|
|
|
|
``MainView`` 以贴合"视图壳"语义,同时按用户原意把 ``TaskWorker`` 从单信号
|
|
|
|
|
|
(``finished_with_result``)升级为三信号协议。
|
|
|
|
|
|
|
2026-06-16 17:53:35 +08:00
|
|
|
|
运行方式
|
|
|
|
|
|
--------
|
2026-06-16 18:23:38 +08:00
|
|
|
|
|
2026-06-16 17:53:35 +08:00
|
|
|
|
::
|
|
|
|
|
|
|
|
|
|
|
|
# 方式 A:作为模块运行(推荐)
|
|
|
|
|
|
cd D:/111/office/ZHLduijie/1.WQ/WQ_GUI
|
2026-06-16 18:23:38 +08:00
|
|
|
|
python -m src.new.main_view
|
2026-06-16 17:53:35 +08:00
|
|
|
|
|
|
|
|
|
|
# 方式 B:直接脚本运行
|
2026-06-16 18:23:38 +08:00
|
|
|
|
python src/new/main_view.py
|
2026-06-16 17:53:35 +08:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
import importlib
|
|
|
|
|
|
import sys
|
|
|
|
|
|
import traceback
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
|
|
from PyQt5.QtCore import Qt, QThread, pyqtSignal
|
|
|
|
|
|
from PyQt5.QtWidgets import (
|
|
|
|
|
|
QFileDialog, QHBoxLayout, QLabel, QListWidget, QMainWindow,
|
|
|
|
|
|
QPushButton, QSplitter, QStackedWidget, QTextEdit, QVBoxLayout,
|
|
|
|
|
|
QWidget,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-06-16 18:23:38 +08:00
|
|
|
|
# 让 ``python src/new/main_view.py`` 直接运行时也能 import ``src.xxx``
|
2026-06-16 17:53:35 +08:00
|
|
|
|
_HERE = Path(__file__).resolve().parent # .../src/new
|
|
|
|
|
|
_SRC_ROOT = _HERE.parent # .../src
|
|
|
|
|
|
if str(_SRC_ROOT) not in sys.path:
|
|
|
|
|
|
sys.path.insert(0, str(_SRC_ROOT))
|
|
|
|
|
|
|
|
|
|
|
|
from src.gui.styles import ModernStylesheet
|
|
|
|
|
|
from src.new.views.placeholder_view import PlaceholderView
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =========================================================================
|
|
|
|
|
|
# 路由注册表:每条路由对应一对 view + service
|
|
|
|
|
|
# =========================================================================
|
|
|
|
|
|
ROUTES = [
|
|
|
|
|
|
{
|
|
|
|
|
|
"id": "step1",
|
|
|
|
|
|
"name": "1. 水域掩膜",
|
|
|
|
|
|
"view_module": "src.new.views.step1_view",
|
|
|
|
|
|
"view_class": "Step1View",
|
|
|
|
|
|
"service_module": "src.new.services.step1_service",
|
|
|
|
|
|
"service_func": "execute_step1",
|
|
|
|
|
|
},
|
2026-06-17 09:15:22 +08:00
|
|
|
|
# -------- 业务路由(前 4 个预处理步骤已迁移到真实 service) --------
|
2026-06-17 08:58:24 +08:00
|
|
|
|
{
|
|
|
|
|
|
"id": "step2",
|
|
|
|
|
|
"name": "2. 耀斑检测",
|
|
|
|
|
|
"view_module": "src.new.views.step2_view",
|
|
|
|
|
|
"view_class": "Step2View",
|
2026-06-17 09:15:22 +08:00
|
|
|
|
"service_module": "src.new.services.step2_service",
|
|
|
|
|
|
"service_func": "execute_step2",
|
2026-06-17 08:58:24 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"id": "step3",
|
|
|
|
|
|
"name": "3. 耀斑去除",
|
|
|
|
|
|
"view_module": "src.new.views.step3_view",
|
|
|
|
|
|
"view_class": "Step3View",
|
2026-06-17 09:15:22 +08:00
|
|
|
|
"service_module": "src.new.services.step3_service",
|
|
|
|
|
|
"service_func": "execute_step3",
|
2026-06-17 08:58:24 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"id": "step4",
|
|
|
|
|
|
"name": "4. 采样点布设",
|
|
|
|
|
|
"view_module": "src.new.views.step4_view",
|
|
|
|
|
|
"view_class": "Step4View",
|
2026-06-17 09:15:22 +08:00
|
|
|
|
"service_module": "src.new.services.step4_service",
|
|
|
|
|
|
"service_func": "execute_step4",
|
2026-06-17 08:58:24 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"id": "step5",
|
|
|
|
|
|
"name": "5. 数据清洗",
|
|
|
|
|
|
"view_module": "src.new.views.step5_view",
|
|
|
|
|
|
"view_class": "Step5View",
|
2026-06-17 09:15:22 +08:00
|
|
|
|
"service_module": "src.new.services.step5_service",
|
|
|
|
|
|
"service_func": "execute_step5",
|
2026-06-17 08:58:24 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"id": "step6",
|
|
|
|
|
|
"name": "6. 光谱特征",
|
|
|
|
|
|
"view_module": "src.new.views.step6_view",
|
|
|
|
|
|
"view_class": "Step6View",
|
|
|
|
|
|
"service_module": "src.new.services.placeholder_service",
|
|
|
|
|
|
"service_func": "execute_placeholder",
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"id": "step7",
|
|
|
|
|
|
"name": "7. 水质光谱指数",
|
|
|
|
|
|
"view_module": "src.new.views.step7_view",
|
|
|
|
|
|
"view_class": "Step7View",
|
|
|
|
|
|
"service_module": "src.new.services.placeholder_service",
|
|
|
|
|
|
"service_func": "execute_placeholder",
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"id": "step8",
|
|
|
|
|
|
"name": "8. 机器学习建模",
|
|
|
|
|
|
"view_module": "src.new.views.step8_view",
|
|
|
|
|
|
"view_class": "Step8View",
|
|
|
|
|
|
"service_module": "src.new.services.placeholder_service",
|
|
|
|
|
|
"service_func": "execute_placeholder",
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"id": "step9",
|
|
|
|
|
|
"name": "9. 机器学习预测",
|
|
|
|
|
|
"view_module": "src.new.views.step9_view",
|
|
|
|
|
|
"view_class": "Step9View",
|
|
|
|
|
|
"service_module": "src.new.services.placeholder_service",
|
|
|
|
|
|
"service_func": "execute_placeholder",
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"id": "step10",
|
|
|
|
|
|
"name": "10. 水色指数反演",
|
|
|
|
|
|
"view_module": "src.new.views.step10_view",
|
|
|
|
|
|
"view_class": "Step10View",
|
|
|
|
|
|
"service_module": "src.new.services.placeholder_service",
|
|
|
|
|
|
"service_func": "execute_placeholder",
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"id": "step11",
|
|
|
|
|
|
"name": "11. 专题图生成",
|
|
|
|
|
|
"view_module": "src.new.views.step11_view",
|
|
|
|
|
|
"view_class": "Step11View",
|
|
|
|
|
|
"service_module": "src.new.services.placeholder_service",
|
|
|
|
|
|
"service_func": "execute_placeholder",
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"id": "step12",
|
|
|
|
|
|
"name": "12. 可视化",
|
|
|
|
|
|
"view_module": "src.new.views.step12_view",
|
|
|
|
|
|
"view_class": "Step12View",
|
|
|
|
|
|
"service_module": "src.new.services.placeholder_service",
|
|
|
|
|
|
"service_func": "execute_placeholder",
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"id": "step13",
|
|
|
|
|
|
"name": "13. 报告生成",
|
|
|
|
|
|
"view_module": "src.new.views.step13_view",
|
|
|
|
|
|
"view_class": "Step13View",
|
|
|
|
|
|
"service_module": "src.new.services.placeholder_service",
|
|
|
|
|
|
"service_func": "execute_placeholder",
|
|
|
|
|
|
},
|
2026-06-16 17:53:35 +08:00
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =========================================================================
|
2026-06-16 18:23:38 +08:00
|
|
|
|
# TaskWorker:QThread 后台执行器(三信号协议)
|
2026-06-16 17:53:35 +08:00
|
|
|
|
# =========================================================================
|
|
|
|
|
|
class TaskWorker(QThread):
|
2026-06-16 18:23:38 +08:00
|
|
|
|
"""后台执行 ``service_func(config)`` 并通过三信号协议回报主线程
|
|
|
|
|
|
|
|
|
|
|
|
信号协议
|
|
|
|
|
|
--------
|
|
|
|
|
|
* ``log_msg(str)`` —— 启动 / 完成 / 异常等关键节点的人类可读日志
|
|
|
|
|
|
* ``finished(dict)`` —— service 正常返回(含 service 内部 try/except
|
|
|
|
|
|
转成的 error 状态 dict)
|
|
|
|
|
|
* ``error(str)`` —— worker 自身捕获的未预期异常(含 traceback),
|
|
|
|
|
|
与 finished 中的 status='error' 语义不同:error 表示 worker
|
|
|
|
|
|
完全崩溃,无法产出任何结果 dict。
|
2026-06-16 17:53:35 +08:00
|
|
|
|
|
|
|
|
|
|
为什么用 QThread 而不是 Thread:service_func 可能涉及 rasterio /
|
|
|
|
|
|
matplotlib 等原生扩展,主线程与子线程的 Qt 事件循环隔离更稳。
|
|
|
|
|
|
"""
|
2026-06-16 18:23:38 +08:00
|
|
|
|
|
|
|
|
|
|
log_msg = pyqtSignal(str)
|
|
|
|
|
|
finished = pyqtSignal(dict)
|
|
|
|
|
|
error = pyqtSignal(str)
|
2026-06-16 17:53:35 +08:00
|
|
|
|
|
|
|
|
|
|
def __init__(self, service_func, config: dict):
|
|
|
|
|
|
super().__init__()
|
|
|
|
|
|
self.service_func = service_func
|
|
|
|
|
|
self.config = config
|
|
|
|
|
|
|
|
|
|
|
|
def run(self):
|
2026-06-16 18:23:38 +08:00
|
|
|
|
func_name = getattr(self.service_func, "__name__", "<anonymous>")
|
|
|
|
|
|
self.log_msg.emit(f"[Worker] 启动 {func_name}")
|
2026-06-16 17:53:35 +08:00
|
|
|
|
try:
|
|
|
|
|
|
result = self.service_func(self.config)
|
2026-06-16 18:23:38 +08:00
|
|
|
|
except Exception as e: # noqa: BLE001 —— worker 兜底捕获所有未预期异常
|
|
|
|
|
|
tb = traceback.format_exc()
|
|
|
|
|
|
self.log_msg.emit(f"[Worker] 异常 {type(e).__name__}: {e}")
|
|
|
|
|
|
self.error.emit(f"{type(e).__name__}: {e}\n{tb}")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
status = result.get("status", "unknown") if isinstance(result, dict) else "unknown"
|
|
|
|
|
|
self.log_msg.emit(f"[Worker] 完成,status={status}")
|
|
|
|
|
|
self.finished.emit(result)
|
2026-06-16 17:53:35 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =========================================================================
|
2026-06-16 18:23:38 +08:00
|
|
|
|
# MainView:QMainWindow 路由壳
|
2026-06-16 17:53:35 +08:00
|
|
|
|
# =========================================================================
|
2026-06-16 18:23:38 +08:00
|
|
|
|
class MainView(QMainWindow):
|
2026-06-16 17:53:35 +08:00
|
|
|
|
"""新架构的轻量路由主窗口"""
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
|
|
super().__init__()
|
|
|
|
|
|
self.work_dir: str = ""
|
|
|
|
|
|
self._views: dict = {}
|
|
|
|
|
|
self._route_map: dict = {}
|
|
|
|
|
|
self.worker: TaskWorker | None = None
|
|
|
|
|
|
|
|
|
|
|
|
self.setWindowTitle("WQ_GUI 新架构 · 端到端模块化路由壳")
|
|
|
|
|
|
self.resize(1280, 800)
|
|
|
|
|
|
self.setStyleSheet(ModernStylesheet.get_main_stylesheet())
|
|
|
|
|
|
|
|
|
|
|
|
self._build_ui()
|
|
|
|
|
|
self._build_routes()
|
|
|
|
|
|
|
2026-06-16 18:23:38 +08:00
|
|
|
|
self._log("[Boot] MainView 初始化完成")
|
2026-06-17 09:15:22 +08:00
|
|
|
|
self._log(f"[Boot] 已注册 {len(ROUTES)} 条路由(13 个 view 全部真实,前 5 个步骤 service 已迁移)")
|
2026-06-16 17:53:35 +08:00
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
|
|
# UI 布局:左侧导航 + 右侧 stacked + 底部日志
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
|
|
def _build_ui(self):
|
|
|
|
|
|
central = QWidget()
|
|
|
|
|
|
self.setCentralWidget(central)
|
|
|
|
|
|
|
|
|
|
|
|
outer = QVBoxLayout(central)
|
|
|
|
|
|
outer.setContentsMargins(8, 8, 8, 8)
|
|
|
|
|
|
|
|
|
|
|
|
# ----- 顶部工具条:工作目录 + 设置按钮 -----
|
|
|
|
|
|
toolbar = QHBoxLayout()
|
|
|
|
|
|
toolbar.addWidget(QLabel("工作目录:"))
|
|
|
|
|
|
self.work_dir_label = QLabel("(未设置)")
|
|
|
|
|
|
self.work_dir_label.setStyleSheet("color: #0055D4; font-weight: bold;")
|
|
|
|
|
|
toolbar.addWidget(self.work_dir_label, 1)
|
|
|
|
|
|
|
|
|
|
|
|
self.set_workdir_btn = QPushButton("设置工作目录...")
|
|
|
|
|
|
self.set_workdir_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet("normal"))
|
|
|
|
|
|
self.set_workdir_btn.clicked.connect(self._on_set_workdir)
|
|
|
|
|
|
toolbar.addWidget(self.set_workdir_btn)
|
|
|
|
|
|
|
|
|
|
|
|
outer.addLayout(toolbar)
|
|
|
|
|
|
|
|
|
|
|
|
# ----- 中部:左导航 + 右 stacked(用 QSplitter 可拖动) -----
|
|
|
|
|
|
splitter = QSplitter(Qt.Horizontal)
|
|
|
|
|
|
|
|
|
|
|
|
self.nav_list = QListWidget()
|
|
|
|
|
|
self.nav_list.setStyleSheet(ModernStylesheet.get_sidebar_stylesheet())
|
|
|
|
|
|
self.nav_list.setFixedWidth(200)
|
|
|
|
|
|
self.nav_list.currentRowChanged.connect(self._on_nav_changed)
|
|
|
|
|
|
splitter.addWidget(self.nav_list)
|
|
|
|
|
|
|
|
|
|
|
|
self.stacked = QStackedWidget()
|
|
|
|
|
|
splitter.addWidget(self.stacked)
|
|
|
|
|
|
|
|
|
|
|
|
splitter.setStretchFactor(0, 0)
|
|
|
|
|
|
splitter.setStretchFactor(1, 1)
|
|
|
|
|
|
outer.addWidget(splitter, 1)
|
|
|
|
|
|
|
|
|
|
|
|
# ----- 底部日志 -----
|
|
|
|
|
|
self.log_text = QTextEdit()
|
|
|
|
|
|
self.log_text.setReadOnly(True)
|
|
|
|
|
|
self.log_text.setStyleSheet("background-color: #1E1E1E; color: #DCDCDC; font-family: Consolas, monospace;")
|
|
|
|
|
|
self.log_text.setFixedHeight(180)
|
|
|
|
|
|
outer.addWidget(self.log_text)
|
|
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
|
|
# 路由注册:实例化 view、填充 nav_list 与 stacked
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
|
|
def _build_routes(self):
|
|
|
|
|
|
for route in ROUTES:
|
|
|
|
|
|
self._route_map[route["id"]] = route
|
|
|
|
|
|
view = self._instantiate_view(route)
|
|
|
|
|
|
self._views[route["id"]] = view
|
|
|
|
|
|
self.stacked.addWidget(view)
|
|
|
|
|
|
self.nav_list.addItem(route["name"])
|
|
|
|
|
|
|
|
|
|
|
|
if ROUTES:
|
|
|
|
|
|
self.nav_list.setCurrentRow(0)
|
|
|
|
|
|
|
|
|
|
|
|
def _instantiate_view(self, route: dict) -> QWidget:
|
|
|
|
|
|
"""根据路由表实例化 view:step1 走真实类,其余走 PlaceholderView"""
|
|
|
|
|
|
if route["view_module"] != "_placeholder":
|
|
|
|
|
|
try:
|
|
|
|
|
|
module = importlib.import_module(route["view_module"])
|
|
|
|
|
|
cls = getattr(module, route["view_class"])
|
|
|
|
|
|
return cls(self.stacked)
|
|
|
|
|
|
except Exception as e: # noqa: BLE001
|
|
|
|
|
|
# 真实 view 加载失败时降级为占位,并在日志区报错
|
|
|
|
|
|
self._log(f"[Router] 加载真实 view 失败 ({route['id']}): {e}")
|
|
|
|
|
|
traceback.print_exc()
|
|
|
|
|
|
|
|
|
|
|
|
# 占位 view:直接构造
|
|
|
|
|
|
return PlaceholderView(route["id"], route["name"], self.stacked)
|
|
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
|
|
# 导航切换
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
|
|
def _on_nav_changed(self, row: int):
|
|
|
|
|
|
if row < 0 or row >= len(ROUTES):
|
|
|
|
|
|
return
|
|
|
|
|
|
route = ROUTES[row]
|
|
|
|
|
|
view = self._views[route["id"]]
|
|
|
|
|
|
self.stacked.setCurrentWidget(view)
|
|
|
|
|
|
self._log(f"[Nav] 切换到 {route['id']} → {route['name']}")
|
|
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
|
|
# 工作目录推送:设置后广播给所有 view
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
|
|
def _on_set_workdir(self):
|
|
|
|
|
|
d = QFileDialog.getExistingDirectory(self, "选择工作目录", self.work_dir or "")
|
|
|
|
|
|
if not d:
|
|
|
|
|
|
return
|
|
|
|
|
|
self.set_work_dir(d)
|
|
|
|
|
|
|
|
|
|
|
|
def set_work_dir(self, work_dir: str):
|
|
|
|
|
|
"""设置工作目录并广播给所有 view"""
|
|
|
|
|
|
if not work_dir:
|
|
|
|
|
|
return
|
|
|
|
|
|
self.work_dir = work_dir.replace("\\", "/")
|
|
|
|
|
|
self.work_dir_label.setText(self.work_dir)
|
|
|
|
|
|
self._log(f"[Router] 工作目录已设置: {self.work_dir}")
|
|
|
|
|
|
|
|
|
|
|
|
# 推送给所有 view(让它们按需自动填充路径)
|
|
|
|
|
|
for view in self._views.values():
|
|
|
|
|
|
if hasattr(view, "update_from_config"):
|
|
|
|
|
|
view.update_from_config(self.work_dir, pipeline=None)
|
|
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
|
|
# 核心调度:run_single_step(被 view.dispatch_execute 调用)
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
|
|
def run_single_step(self, step_id: str, config_dict: dict):
|
|
|
|
|
|
"""从 view 接收 ``(step_id, config)`` → 调度 service 后台执行
|
|
|
|
|
|
|
|
|
|
|
|
1. 查路由表;
|
|
|
|
|
|
2. 注入 ``work_dir``(避免 view 重复感知);
|
|
|
|
|
|
3. 启动 ``TaskWorker`` 在子线程运行 service;
|
2026-06-16 18:23:38 +08:00
|
|
|
|
4. 三信号路由:log_msg → 日志框;finished → 状态分支日志;
|
|
|
|
|
|
error → 未预期异常写日志 + traceback。
|
2026-06-16 17:53:35 +08:00
|
|
|
|
"""
|
|
|
|
|
|
route = self._route_map.get(step_id)
|
|
|
|
|
|
if route is None:
|
|
|
|
|
|
self._log(f"[Router] 未知 step_id: {step_id}")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
2026-06-16 18:23:38 +08:00
|
|
|
|
# 注入 work_dir(仅当 view 未带且 main_view 已配置时)
|
2026-06-16 17:53:35 +08:00
|
|
|
|
if self.work_dir and "work_dir" not in config_dict:
|
|
|
|
|
|
config_dict = {**config_dict, "work_dir": self.work_dir}
|
|
|
|
|
|
|
|
|
|
|
|
service_func = self._load_callable(route["service_module"], route["service_func"])
|
|
|
|
|
|
if service_func is None:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
self._log(f"[Router] 收到 {step_id} 请求,配置: {config_dict}")
|
|
|
|
|
|
|
|
|
|
|
|
# 启动后台线程(覆盖旧 worker 防泄漏)
|
|
|
|
|
|
if self.worker and self.worker.isRunning():
|
|
|
|
|
|
self._log("[Router] 上一个任务尚未结束,丢弃新请求")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
self.worker = TaskWorker(service_func, config_dict)
|
2026-06-16 18:23:38 +08:00
|
|
|
|
self.worker.log_msg.connect(self._log)
|
|
|
|
|
|
self.worker.finished.connect(self._on_step_done)
|
|
|
|
|
|
self.worker.error.connect(self._on_step_error)
|
2026-06-16 17:53:35 +08:00
|
|
|
|
self.worker.start()
|
|
|
|
|
|
|
|
|
|
|
|
def _load_callable(self, module_name: str, attr_name: str):
|
|
|
|
|
|
"""动态加载 ``module_name.attr_name``,失败时写日志并返回 None"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
module = importlib.import_module(module_name)
|
|
|
|
|
|
return getattr(module, attr_name)
|
|
|
|
|
|
except Exception as e: # noqa: BLE001
|
|
|
|
|
|
self._log(f"[Router] 加载 service 失败 ({module_name}.{attr_name}): {e}")
|
|
|
|
|
|
traceback.print_exc()
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
def _on_step_done(self, result: dict):
|
2026-06-16 18:23:38 +08:00
|
|
|
|
"""TaskWorker finished 回调——按 status 分支写日志
|
|
|
|
|
|
|
|
|
|
|
|
注:service 内部 try/except 转换的 status='error' 也走这里;
|
|
|
|
|
|
这与 error 信号(worker 自身崩溃)语义不同。
|
|
|
|
|
|
"""
|
2026-06-16 17:53:35 +08:00
|
|
|
|
status = result.get("status", "unknown")
|
|
|
|
|
|
message = result.get("message", "")
|
|
|
|
|
|
output_path = result.get("output_path")
|
|
|
|
|
|
mode = result.get("mode", "")
|
|
|
|
|
|
|
|
|
|
|
|
if status == "completed":
|
|
|
|
|
|
self._log(f"[Service✓] {message}(mode={mode}, output={output_path})")
|
|
|
|
|
|
elif status == "skipped":
|
|
|
|
|
|
self._log(f"[Service↻] {message}(已复用历史结果)")
|
|
|
|
|
|
elif status == "not_implemented":
|
|
|
|
|
|
self._log(f"[Service…] {message}")
|
2026-06-16 18:23:38 +08:00
|
|
|
|
else: # error(service 内部已转 dict)
|
2026-06-16 17:53:35 +08:00
|
|
|
|
self._log(f"[Service✗] {message}(mode={mode or 'n/a'})")
|
|
|
|
|
|
tb = result.get("traceback")
|
|
|
|
|
|
if tb:
|
|
|
|
|
|
self._log(tb)
|
|
|
|
|
|
|
2026-06-16 18:23:38 +08:00
|
|
|
|
def _on_step_error(self, msg: str):
|
|
|
|
|
|
"""TaskWorker error 信号回调——worker 自身捕获的未预期异常"""
|
|
|
|
|
|
self._log(f"[Worker✗] 未预期异常:\n{msg}")
|
|
|
|
|
|
|
2026-06-16 17:53:35 +08:00
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
|
|
# 日志:所有日志统一走这里
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
|
|
def _log(self, msg: str):
|
|
|
|
|
|
# 控制台 + 底部日志框
|
|
|
|
|
|
print(msg)
|
|
|
|
|
|
self.log_text.append(msg)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =========================================================================
|
|
|
|
|
|
# 入口
|
|
|
|
|
|
# =========================================================================
|
|
|
|
|
|
def main():
|
|
|
|
|
|
from PyQt5.QtWidgets import QApplication
|
|
|
|
|
|
app = QApplication(sys.argv)
|
2026-06-16 18:23:38 +08:00
|
|
|
|
win = MainView()
|
2026-06-16 17:53:35 +08:00
|
|
|
|
win.show()
|
|
|
|
|
|
sys.exit(app.exec_())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
2026-06-16 18:23:38 +08:00
|
|
|
|
main()
|