格式统一
This commit is contained in:
@ -92,6 +92,14 @@ class ConfigManager(QObject):
|
||||
)
|
||||
return
|
||||
|
||||
# ★ 验证 JSON 结构:必须是 dict 类型
|
||||
if not isinstance(config, dict):
|
||||
QMessageBox.critical(
|
||||
self.parent(), "配置格式错误",
|
||||
f"配置文件格式不正确(期望 JSON 对象,实际为 {type(config).__name__})。\n请确认选择了正确的配置文件。"
|
||||
)
|
||||
return
|
||||
|
||||
# 回填已加载面板
|
||||
loaded_count = 0
|
||||
for step_id, panel in self._panel_factory.get_loaded_panels().items():
|
||||
@ -99,8 +107,11 @@ class ConfigManager(QObject):
|
||||
try:
|
||||
panel.set_config(config[step_id])
|
||||
loaded_count += 1
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
global_event_bus.publish('LogMessage', {
|
||||
'message': f'[警告] 面板 {step_id} 加载配置失败: {e}',
|
||||
'level': 'warning',
|
||||
})
|
||||
|
||||
global_event_bus.publish('LogMessage', {
|
||||
'message': f'已加载配置: {file_path}(回填 {loaded_count} 个面板)',
|
||||
@ -156,6 +167,10 @@ class ConfigManager(QObject):
|
||||
if hasattr(panel, 'get_config'):
|
||||
try:
|
||||
config[step_id] = panel.get_config()
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
config[step_id] = {}
|
||||
global_event_bus.publish('LogMessage', {
|
||||
'message': f'[警告] 面板 {step_id} 获取配置失败: {e}',
|
||||
'level': 'warning',
|
||||
})
|
||||
return config
|
||||
|
||||
@ -16,12 +16,17 @@ PANEL_REGISTRY 中声明的 dependencies 自动向 global_event_bus
|
||||
- sip.isdeleted() 保护:面板销毁后回调自动跳过,避免 C++ 野指针 Segfault
|
||||
- 非空保护:仅在目标框为空时填充,避免覆盖用户已选路径
|
||||
- 智能目录转换:目标控件名含 'dir' 且事件携带的是文件路径时,自动取父目录
|
||||
- ★ panel.destroyed 信号自动取消订阅(2026-06-30)
|
||||
- ★ weakref 防止闭包阻止 panel GC(2026-06-30)
|
||||
|
||||
2026-06-30 修复:
|
||||
- 添加 sip.isdeleted(panel) 检查,防止面板删除后访问 C++ 对象导致崩溃
|
||||
- 连接 panel.destroyed 信号自动清理订阅,彻底杜绝回调泄漏
|
||||
- 使用 weakref.ref 包装回调中的 panel 引用,防止 GC 阻塞
|
||||
"""
|
||||
|
||||
import os
|
||||
import weakref
|
||||
import sip
|
||||
|
||||
from src.gui.core.event_bus import global_event_bus
|
||||
@ -48,8 +53,18 @@ def subscribe_panel_to_dependencies(panel, step_id, dependencies):
|
||||
return
|
||||
|
||||
panel_id = id(panel)
|
||||
if panel_id not in _subscription_registry:
|
||||
_subscription_registry[panel_id] = []
|
||||
# ★ 若之前已订阅过,先清理旧订阅(防止重复订阅泄漏)
|
||||
if panel_id in _subscription_registry:
|
||||
unsubscribe_panel_from_dependencies(panel)
|
||||
|
||||
_subscription_registry[panel_id] = []
|
||||
|
||||
# ★ 面板销毁时自动清理订阅,杜绝闭包泄漏
|
||||
if hasattr(panel, 'destroyed'):
|
||||
try:
|
||||
panel.destroyed.connect(lambda obj=None: _on_panel_destroyed(panel_id))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for _input_field, (dep_step, output_type, source_panel_attr) in dependencies.items():
|
||||
callback = _make_callback(panel, dep_step, output_type, _input_field)
|
||||
@ -57,6 +72,13 @@ def subscribe_panel_to_dependencies(panel, step_id, dependencies):
|
||||
_subscription_registry[panel_id].append(('OutputUpdated', callback))
|
||||
|
||||
|
||||
def _on_panel_destroyed(panel_id):
|
||||
"""panel.destroyed 信号回调:清理该面板的所有订阅。"""
|
||||
subscriptions = _subscription_registry.pop(panel_id, [])
|
||||
for event_name, callback in subscriptions:
|
||||
global_event_bus.unsubscribe(event_name, callback)
|
||||
|
||||
|
||||
def unsubscribe_panel_from_dependencies(panel):
|
||||
"""取消面板的所有依赖订阅。应在面板销毁前调用,防止野指针回调。
|
||||
|
||||
@ -72,13 +94,20 @@ def unsubscribe_panel_from_dependencies(panel):
|
||||
def _make_callback(panel, dep_step, output_type, target_widget_name):
|
||||
"""为单个依赖项创建事件回调。返回闭包函数。
|
||||
|
||||
使用工厂函数确保每个回调的闭包变量独立绑定。
|
||||
使用 weakref 持有 panel 引用,防止闭包阻止 panel GC。
|
||||
同时检查 panel 和 widget 的 C++ 对象是否存活。
|
||||
"""
|
||||
|
||||
panel_ref = weakref.ref(panel)
|
||||
|
||||
def callback(data):
|
||||
p = panel_ref()
|
||||
if p is None:
|
||||
return
|
||||
|
||||
# ★ 防野指针:面板底层 C++ 对象已销毁时直接跳过
|
||||
try:
|
||||
if sip.isdeleted(panel):
|
||||
if sip.isdeleted(p):
|
||||
return
|
||||
except Exception:
|
||||
return
|
||||
@ -88,10 +117,17 @@ def _make_callback(panel, dep_step, output_type, target_widget_name):
|
||||
if data.get('output_type') != output_type:
|
||||
return
|
||||
|
||||
widget = getattr(panel, target_widget_name, None)
|
||||
widget = getattr(p, target_widget_name, None)
|
||||
if widget is None:
|
||||
return
|
||||
|
||||
# ★ 检查 widget 自身的 C++ 对象是否存活
|
||||
try:
|
||||
if sip.isdeleted(widget):
|
||||
return
|
||||
except Exception:
|
||||
return
|
||||
|
||||
current = ''
|
||||
try:
|
||||
if hasattr(widget, 'get_path'):
|
||||
|
||||
@ -14,6 +14,10 @@
|
||||
from PyQt5.QtCore import QObject
|
||||
from PyQt5.QtWidgets import QMessageBox
|
||||
|
||||
# ★ 集中版本号常量
|
||||
APP_VERSION = "V1.2"
|
||||
APP_NAME = "MegaCube-Water Quality"
|
||||
|
||||
|
||||
class DialogService(QObject):
|
||||
"""对话框服务。
|
||||
@ -53,7 +57,7 @@ class DialogService(QObject):
|
||||
"""显示"关于"对话框。"""
|
||||
QMessageBox.about(
|
||||
self.parent(), "关于",
|
||||
"MegaCube-Water Quality V1.2\n\n"
|
||||
f"{APP_NAME} {APP_VERSION}\n\n"
|
||||
"一个完整的水质参数反演工作流程工具\n\n"
|
||||
"公司:北京依锐思遥感技术有限公司\n"
|
||||
"地址:北京市海淀区清河安宁庄东路18号5号楼二层205\n"
|
||||
|
||||
@ -11,16 +11,19 @@
|
||||
- 新增 unsubscribe() 方法,允许面板销毁时清理订阅,防止内存泄漏和野指针回调
|
||||
"""
|
||||
|
||||
import sys
|
||||
import threading
|
||||
import traceback
|
||||
from collections import defaultdict
|
||||
from typing import Any, Callable, Dict, List, Optional
|
||||
|
||||
|
||||
class EventBus:
|
||||
"""发布-订阅事件总线"""
|
||||
"""发布-订阅事件总线(线程安全)。"""
|
||||
|
||||
def __init__(self):
|
||||
self._subscribers: Dict[str, List[Callable]] = defaultdict(list)
|
||||
self._lock = threading.Lock()
|
||||
# 异常日志回调(可选注入,供 LogManager 使用)
|
||||
self._error_logger: Optional[Callable[[str], None]] = None
|
||||
|
||||
@ -30,38 +33,42 @@ class EventBus:
|
||||
|
||||
def subscribe(self, event_name: str, callback: Callable[[dict], None]):
|
||||
"""订阅事件。callback 接收一个 dict 作为事件数据。"""
|
||||
if callback not in self._subscribers[event_name]:
|
||||
self._subscribers[event_name].append(callback)
|
||||
with self._lock:
|
||||
if callback not in self._subscribers[event_name]:
|
||||
self._subscribers[event_name].append(callback)
|
||||
|
||||
def unsubscribe(self, event_name: str, callback: Callable[[dict], None]):
|
||||
"""取消订阅。面板销毁或不再需要接收事件时调用,防止野指针回调。"""
|
||||
subs = self._subscribers.get(event_name)
|
||||
if subs and callback in subs:
|
||||
subs.remove(callback)
|
||||
# 清理空列表,避免字典膨胀
|
||||
if not subs:
|
||||
del self._subscribers[event_name]
|
||||
with self._lock:
|
||||
subs = self._subscribers.get(event_name)
|
||||
if subs and callback in subs:
|
||||
subs.remove(callback)
|
||||
# 清理空列表,避免字典膨胀
|
||||
if not subs:
|
||||
del self._subscribers[event_name]
|
||||
|
||||
def publish(self, event_name: str, data: Dict[str, Any]):
|
||||
"""发布事件,通知所有订阅者。订阅者异常不再静默吞掉,而是输出 traceback。
|
||||
|
||||
迭代订阅者列表的副本,防止回调中调用 unsubscribe() 导致跳过后续订阅者。
|
||||
"""
|
||||
for callback in list(self._subscribers.get(event_name, [])):
|
||||
with self._lock:
|
||||
snapshot = list(self._subscribers.get(event_name, []))
|
||||
for callback in snapshot:
|
||||
try:
|
||||
callback(data)
|
||||
except Exception:
|
||||
cb_name = getattr(callback, '__name__', None) or repr(callback)
|
||||
err_msg = (
|
||||
f"[EventBus] 事件 '{event_name}' 的订阅者 {callback.__name__!r} 抛出异常:\n"
|
||||
f"[EventBus] 事件 '{event_name}' 的订阅者 {cb_name!r} 抛出异常:\n"
|
||||
+ traceback.format_exc()
|
||||
)
|
||||
if self._error_logger:
|
||||
try:
|
||||
self._error_logger(err_msg)
|
||||
except Exception:
|
||||
pass
|
||||
print(err_msg, file=sys.stderr, flush=True)
|
||||
else:
|
||||
import sys
|
||||
print(err_msg, file=sys.stderr, flush=True)
|
||||
|
||||
|
||||
|
||||
@ -16,6 +16,7 @@
|
||||
ProgressUpdate → {percentage, message} 更新进度条
|
||||
"""
|
||||
|
||||
import html
|
||||
from datetime import datetime
|
||||
|
||||
from PyQt5.QtCore import QObject
|
||||
@ -27,6 +28,8 @@ from PyQt5.QtWidgets import (
|
||||
|
||||
from src.gui.core.event_bus import global_event_bus
|
||||
|
||||
_MAX_LOG_LINES = 2000 # 日志行数上限,防止长时间运行内存耗尽
|
||||
|
||||
|
||||
class LogManager(QObject):
|
||||
"""日志与进度管理器。
|
||||
@ -48,6 +51,15 @@ class LogManager(QObject):
|
||||
global_event_bus.subscribe('LogMessage', self._on_log_message)
|
||||
global_event_bus.subscribe('ProgressUpdate', self._on_progress_update)
|
||||
|
||||
# ★ 父控件销毁时自动取消订阅,防止回调泄漏
|
||||
if parent is not None:
|
||||
parent.destroyed.connect(self._cleanup)
|
||||
|
||||
def _cleanup(self):
|
||||
"""清理 EventBus 订阅。"""
|
||||
global_event_bus.unsubscribe('LogMessage', self._on_log_message)
|
||||
global_event_bus.unsubscribe('ProgressUpdate', self._on_progress_update)
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# 公开 API
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
@ -168,13 +180,23 @@ class LogManager(QObject):
|
||||
"""LogMessage 事件回调:写入日志区。"""
|
||||
if self._log_text is None:
|
||||
return
|
||||
if not isinstance(data, dict):
|
||||
return
|
||||
message = data.get('message', '')
|
||||
level = data.get('level', 'info')
|
||||
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
# ★ HTML 转义:防止日志消息中的 < > & 破坏 HTML 渲染
|
||||
safe_message = html.escape(str(message))
|
||||
color_map = {'error': 'red', 'warning': 'orange'}
|
||||
color = color_map.get(level, 'black')
|
||||
formatted = f'<span style="color: {color};">[{timestamp}] {message}</span>'
|
||||
formatted = f'<span style="color: {color};">[{timestamp}] {safe_message}</span>'
|
||||
self._log_text.append(formatted)
|
||||
# ★ 日志行数上限:防止长时间运行内存耗尽
|
||||
if self._log_text.document().blockCount() > _MAX_LOG_LINES + 100:
|
||||
self._log_text.clear()
|
||||
self._log_text.append(
|
||||
f'<span style="color: gray;">[日志已自动清空,达到 {_MAX_LOG_LINES} 行上限]</span>'
|
||||
)
|
||||
cursor = self._log_text.textCursor()
|
||||
cursor.movePosition(QTextCursor.End)
|
||||
self._log_text.setTextCursor(cursor)
|
||||
|
||||
@ -202,6 +202,8 @@ class PanelFactory:
|
||||
self._tab_widget.removeTab(tab_index)
|
||||
self._tab_widget.insertTab(tab_index, scroll, tab_icon, tab_title)
|
||||
self._tab_widget.setCurrentIndex(current_active) # 恢复原来的 Tab,严禁后台预加载引发跳页!
|
||||
# ★ 显式释放占位页,防止泄漏
|
||||
placeholder.deleteLater()
|
||||
finally:
|
||||
self._tab_widget.blockSignals(False)
|
||||
|
||||
@ -260,6 +262,9 @@ class PanelFactory:
|
||||
# 3. 实时扫描已加载面板中被依赖的属性(覆盖全局输入如 reference_img)
|
||||
self._replay_live_panel_inputs()
|
||||
|
||||
# ★ 重新禁用滚轮:update_from_config 可能动态创建了新的 SpinBox/ComboBox
|
||||
_disable_wheel_for_widget(panel)
|
||||
|
||||
def _replay_live_panel_inputs(self):
|
||||
"""遍历 PANEL_REGISTRY 依赖声明,从已加载面板实时读取属性值并强制广播。
|
||||
|
||||
@ -290,7 +295,7 @@ class PanelFactory:
|
||||
if not path: continue
|
||||
|
||||
# 核心修复:强制转为绝对路径,防止跨目录传递时路径丢失
|
||||
absolute_path = os.path.abspath(path).replace('\\', '/')
|
||||
absolute_path = os.path.normpath(os.path.abspath(path))
|
||||
|
||||
# 2026-06-30:仅当文件/目录确实存在时才广播,阻断幽灵路径级联
|
||||
if not os.path.exists(absolute_path):
|
||||
|
||||
@ -167,7 +167,7 @@ PANEL_REGISTRY = [
|
||||
'step_id': 'step9_ml_predict',
|
||||
'class_ref': Step9MlPredictPanel,
|
||||
'title': '机器学习预测',
|
||||
'icon': '10.png',
|
||||
'icon': '9.png',
|
||||
'stage': '模块三 模型训练与反演',
|
||||
'display_name': '9. 机器学习预测',
|
||||
# 架构解耦(2026-06-22):dict 键名 = 下游目标控件真实属性名;
|
||||
@ -198,7 +198,7 @@ PANEL_REGISTRY = [
|
||||
'step_id': 'step11_map',
|
||||
'class_ref': Step11MapPanel,
|
||||
'title': '专题图生成',
|
||||
'icon': '10.png',
|
||||
'icon': '11.png',
|
||||
'stage': '模块四 制图与成果汇编',
|
||||
'display_name': '11. 分布图生成',
|
||||
# 架构解耦(2026-06-22):dict 键名 = 下游目标控件真实属性名
|
||||
@ -226,7 +226,7 @@ PANEL_REGISTRY = [
|
||||
'step_id': 'step13_report',
|
||||
'class_ref': Step13ReportPanel,
|
||||
'title': '报告生成',
|
||||
'icon': '10.png',
|
||||
'icon': '13.png',
|
||||
'stage': '模块四 制图与成果汇编',
|
||||
'display_name': '13. 分析报告生成',
|
||||
'dependencies': None,
|
||||
|
||||
@ -118,6 +118,18 @@ class PipelineExecutor(QObject):
|
||||
# ★ 终端即时反馈:确保即使 EventBus/日志区未就绪也能看到
|
||||
print("\n[PipelineExecutor] 收到「运行完整流程」指令,开始执行...")
|
||||
|
||||
# ★ 防重入:如果已有 Worker 在运行,拒绝启动
|
||||
if self.is_running:
|
||||
global_event_bus.publish('LogMessage', {
|
||||
'message': '[警告] 后台任务正在运行中,请等待完成或手动停止后再启动新流程。',
|
||||
'level': 'warning',
|
||||
})
|
||||
QMessageBox.warning(
|
||||
self.parent(), "后台忙碌",
|
||||
"后台任务正在运行中,无法启动新流程。\n请等待当前任务完成或点击「停止」按钮后再试。"
|
||||
)
|
||||
return
|
||||
|
||||
if not PIPELINE_AVAILABLE:
|
||||
global_event_bus.publish('LogMessage', {
|
||||
'message': '无法导入 Pipeline 模块,请检查项目文件结构!',
|
||||
@ -340,6 +352,18 @@ class PipelineExecutor(QObject):
|
||||
)
|
||||
|
||||
def _run_single_step_impl(self, step_name: str, config: dict = None):
|
||||
# ★ 防重入:如果已有 Worker 在运行,拒绝启动
|
||||
if self.is_running:
|
||||
global_event_bus.publish('LogMessage', {
|
||||
'message': '[警告] 后台任务正在运行中,请等待完成或手动停止后再启动新步骤。',
|
||||
'level': 'warning',
|
||||
})
|
||||
QMessageBox.warning(
|
||||
self.parent(), "后台忙碌",
|
||||
"后台任务正在运行中,无法启动新步骤。\n请等待当前任务完成或点击「停止」按钮后再试。"
|
||||
)
|
||||
return
|
||||
|
||||
if not PIPELINE_AVAILABLE:
|
||||
global_event_bus.publish('LogMessage', {
|
||||
'message': '无法导入 Pipeline 模块,请检查 src/core/handlers/ 目录是否完整!',
|
||||
|
||||
@ -45,6 +45,10 @@ class TrainingModeManager(QObject):
|
||||
Args:
|
||||
checked: True=有训练数据模式, False=无训练数据模式
|
||||
"""
|
||||
# ★ 防护:相同状态不重复发布事件
|
||||
if self._training_mode == checked:
|
||||
return
|
||||
|
||||
self._training_mode = checked
|
||||
|
||||
global_event_bus.publish('LogMessage', {
|
||||
|
||||
@ -73,7 +73,7 @@ class VisualizationWorkerThread(QThread):
|
||||
wp = Path(self.work_dir)
|
||||
if self.task == "mask_glint":
|
||||
from src.postprocessing.visualization_reports import WaterQualityVisualization
|
||||
viz = WaterQualityVisualization(output_dir=str(wp / "14_visualization"))
|
||||
viz = WaterQualityVisualization(output_dir=str(wp / "12_visualization"))
|
||||
preview_paths = viz.generate_glint_deglint_previews(
|
||||
work_dir=str(wp),
|
||||
output_subdir="glint_deglint_previews",
|
||||
@ -109,7 +109,7 @@ class VisualizationWorkerThread(QThread):
|
||||
csv_path = str(csv_files[0])
|
||||
from src.postprocessing.point_map import SamplingPointMap
|
||||
map_generator = SamplingPointMap(
|
||||
output_dir=str(wp / "14_visualization" / "sampling_maps"),
|
||||
output_dir=str(wp / "12_visualization" / "sampling_maps"),
|
||||
fast_mode=True,
|
||||
)
|
||||
map_path = map_generator.create_sampling_point_map(
|
||||
@ -134,7 +134,7 @@ class VisualizationWorkerThread(QThread):
|
||||
)
|
||||
elif self.task == "spectrum":
|
||||
from src.postprocessing.visualization_reports import WaterQualityVisualization
|
||||
viz = WaterQualityVisualization(output_dir=str(wp / "14_visualization"))
|
||||
viz = WaterQualityVisualization(output_dir=str(wp / "12_visualization"))
|
||||
csv_file = self.extra.get("csv_path")
|
||||
wl = self.extra.get("wavelength_start_column", "UTM_Y")
|
||||
n_groups = int(self.extra.get("n_groups", 5))
|
||||
@ -178,7 +178,7 @@ class VisualizationWorkerThread(QThread):
|
||||
)
|
||||
elif self.task == "statistics":
|
||||
from src.postprocessing.visualization_reports import WaterQualityVisualization
|
||||
viz = WaterQualityVisualization(output_dir=str(wp / "14_visualization"))
|
||||
viz = WaterQualityVisualization(output_dir=str(wp / "12_visualization"))
|
||||
csv_file = self.extra.get("csv_path")
|
||||
param_cols = self.extra.get("param_cols") or []
|
||||
output_paths = viz.plot_statistical_charts(
|
||||
@ -206,7 +206,7 @@ class VisualizationWorkerThread(QThread):
|
||||
self.finished_ok.emit({"task": "scatter", "scatter_paths": scatter_paths or {}})
|
||||
elif self.task == "generate_all_selected":
|
||||
from src.postprocessing.visualization_reports import WaterQualityVisualization
|
||||
viz = WaterQualityVisualization(output_dir=str(wp / "14_visualization"))
|
||||
viz = WaterQualityVisualization(output_dir=str(wp / "12_visualization"))
|
||||
parts = []
|
||||
|
||||
training_csv_path = (self.extra.get("training_csv_path") or "").strip()
|
||||
@ -333,7 +333,7 @@ class VisualizationWorkerThread(QThread):
|
||||
csv_path = str(csv_files[0])
|
||||
from src.postprocessing.point_map import SamplingPointMap
|
||||
map_generator = SamplingPointMap(
|
||||
output_dir=str(wp / "14_visualization" / "sampling_maps"),
|
||||
output_dir=str(wp / "12_visualization" / "sampling_maps"),
|
||||
fast_mode=True,
|
||||
)
|
||||
map_path = map_generator.create_sampling_point_map(
|
||||
|
||||
@ -28,8 +28,8 @@ import sys
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from PyQt5.QtCore import QObject
|
||||
from PyQt5.QtWidgets import QMessageBox, QFileDialog
|
||||
from PyQt5.QtCore import QObject, Qt, QThread, QTimer
|
||||
from PyQt5.QtWidgets import QMessageBox, QFileDialog, QApplication
|
||||
|
||||
from src.gui.core.event_bus import global_event_bus
|
||||
from src.core.workspace_manager import WorkspaceManager
|
||||
@ -157,10 +157,21 @@ class WorkspaceInitializer(QObject):
|
||||
panel.set_work_dir(dir_path)
|
||||
|
||||
def open_work_directory(self):
|
||||
"""在资源管理器中打开工作目录。"""
|
||||
"""在资源管理器中打开工作目录(跨平台)。"""
|
||||
import subprocess
|
||||
import platform
|
||||
work_dir = self._work_dir or './work_dir'
|
||||
if os.path.exists(work_dir):
|
||||
os.startfile(work_dir)
|
||||
try:
|
||||
system = platform.system()
|
||||
if system == 'Windows':
|
||||
os.startfile(work_dir)
|
||||
elif system == 'Darwin':
|
||||
subprocess.Popen(['open', work_dir])
|
||||
else:
|
||||
subprocess.Popen(['xdg-open', work_dir])
|
||||
except Exception as e:
|
||||
QMessageBox.warning(self.parent(), "警告", f"无法打开工作目录: {e}")
|
||||
else:
|
||||
QMessageBox.warning(self.parent(), "警告", "工作目录不存在!")
|
||||
|
||||
@ -205,8 +216,11 @@ class WorkspaceInitializer(QObject):
|
||||
continue
|
||||
try:
|
||||
panel.update_from_config(work_dir=work_dir, pipeline=None)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
global_event_bus.publish('LogMessage', {
|
||||
'message': f'[警告] 面板 {step_id} 自动填充失败: {e}',
|
||||
'level': 'warning',
|
||||
})
|
||||
|
||||
global_event_bus.publish('LogMessage', {
|
||||
'message': '✓ 工作目录扫描完成,事件总线已通知所有面板自动填充',
|
||||
@ -223,7 +237,15 @@ class WorkspaceInitializer(QObject):
|
||||
当 PipelineExecutor 发布 StepCompleted 事件时,
|
||||
此方法调用 WorkspaceManager.update_step_outputs() 扫描磁盘,
|
||||
WorkspaceManager 内部自动发布 OutputUpdated 事件驱动面板填充。
|
||||
|
||||
★ 线程安全:若回调发生在非主线程(如 worker 线程),
|
||||
通过 QTimer.singleShot(0) 编组到主线程再执行。
|
||||
"""
|
||||
# 线程安全检查:非主线程时编组到主线程
|
||||
if QApplication.instance() and QApplication.instance().thread() != QThread.currentThread():
|
||||
QTimer.singleShot(0, lambda: self._on_step_completed(data))
|
||||
return
|
||||
|
||||
if not data.get('success'):
|
||||
return
|
||||
|
||||
|
||||
Reference in New Issue
Block a user