格式统一
This commit is contained in:
@ -51,12 +51,28 @@ class EventBus:
|
||||
"""发布事件,通知所有订阅者。订阅者异常不再静默吞掉,而是输出 traceback。
|
||||
|
||||
迭代订阅者列表的副本,防止回调中调用 unsubscribe() 导致跳过后续订阅者。
|
||||
|
||||
2026-07-01 修复:捕获 RuntimeError(sip 野指针异常)防止已删除的 C++ 对象回调崩溃。
|
||||
"""
|
||||
with self._lock:
|
||||
snapshot = list(self._subscribers.get(event_name, []))
|
||||
for callback in snapshot:
|
||||
try:
|
||||
callback(data)
|
||||
except RuntimeError:
|
||||
# ★ 野指针保护:C++ 对象已删除时 sip 抛出 RuntimeError,静默跳过。
|
||||
cb_name = getattr(callback, '__name__', None) or repr(callback)
|
||||
err_msg = (
|
||||
f"[EventBus] 事件 '{event_name}' 的订阅者 {cb_name!r} C++ 对象已销毁,跳过。\n"
|
||||
+ traceback.format_exc()
|
||||
)
|
||||
if self._error_logger:
|
||||
try:
|
||||
self._error_logger(err_msg)
|
||||
except Exception:
|
||||
print(err_msg, file=sys.stderr, flush=True)
|
||||
else:
|
||||
print(err_msg, file=sys.stderr, flush=True)
|
||||
except Exception:
|
||||
cb_name = getattr(callback, '__name__', None) or repr(callback)
|
||||
err_msg = (
|
||||
|
||||
@ -537,17 +537,28 @@ class PipelineExecutor(QObject):
|
||||
def _check_worker_health(self):
|
||||
"""看门狗巡检:每 5 秒触发一次。
|
||||
|
||||
2026-07-01 修复:优先尝试优雅停止再 fallback terminate()
|
||||
|
||||
判定逻辑:
|
||||
- Worker 已不在运行(自然结束/被强杀) → 停掉看门狗
|
||||
- Worker 仍在运行 + 上次进度距今 > 600 秒 → 判定为假死/死锁,
|
||||
强制 terminate() 并通过 _on_finished(False, ...) 汇流解 UI,
|
||||
让被卡死的「独立运行此步骤」按钮恢复可用。
|
||||
先尝试 WorkerThread.stop() 优雅停止(5 秒超时),
|
||||
若优雅停止也超时则 fallback 到 terminate()
|
||||
"""
|
||||
import time
|
||||
if self._worker and self._worker.isRunning():
|
||||
if time.time() - self._last_progress_time > 600:
|
||||
self._log_message("[错误] 后台任务响应超时(超过10分钟无响应),强制终止...", "error")
|
||||
self._worker.terminate()
|
||||
self._log_message("[错误] 后台任务响应超时(超过10分钟无响应),尝试优雅停止...", "error")
|
||||
# 先尝试通过 cancel + wait 优雅停止
|
||||
if hasattr(self._worker, 'stop'):
|
||||
try:
|
||||
self._worker.stop()
|
||||
except Exception:
|
||||
pass
|
||||
# 如果优雅停止后线程仍在运行,再 fallback 到 terminate()
|
||||
if self._worker.isRunning():
|
||||
self._log_message("[错误] 优雅停止无效,执行强制终止(terminate)...", "error")
|
||||
self._worker.terminate()
|
||||
self._on_finished(False, "后台任务响应超时或发生底层段错误,已强制终止。")
|
||||
else:
|
||||
# Worker 已退出(自然结束 / 已 terminate),关闭看门狗
|
||||
|
||||
@ -304,11 +304,11 @@ class PreflightDialog(QDialog):
|
||||
def build_missing_items(config: dict) -> List[MissingItem]:
|
||||
"""DAG-aware 预检:从 config 构建缺失项列表。
|
||||
|
||||
2026-06-30 修复(步骤 ID 错配):
|
||||
config 的 key 是 PANEL_REGISTRY 的 step_id(如 step4_sampling),
|
||||
而 PIPELINE_STEPS 使用另一套 step_id(如 step4)。
|
||||
现通过 _PIPELINE_TO_CONFIG_KEY 映射表统一翻译,
|
||||
避免 preflight 永远误报缺失项。
|
||||
2026-07-01 修复(变量命名混乱 + 缺少 step4_sampling CSV 预检):
|
||||
- 将变量名从旧 PIPELINE_STEPS 编号改为 PANEL_REGISTRY step_id
|
||||
- 新增 step4_sampling 采样点 CSV 文件的显式预检
|
||||
- 将 _TAB_INDEX_MAP 提升到顶级并与 STEP_TAB_MAP 合并使用
|
||||
- _cfg() 映射表保留用于翻译旧 PIPELINE_STEPS step_id → 新 PANEL_REGISTRY step_id
|
||||
|
||||
拓扑预判逻辑:
|
||||
1. 按 pipeline 顺序遍历所有 enabled=True 的步骤,收集其 produces 列表,
|
||||
@ -318,17 +318,12 @@ class PreflightDialog(QDialog):
|
||||
- 若 key 在 dynamically_produced_keys 中 → OK(前置步骤会生成)
|
||||
- 否则 → MissingItem(真正缺失)
|
||||
3. 智能免检规则:
|
||||
- formula_csv_path:底层的完全可选参数,任何情况下都免检。
|
||||
- step5 boundary_path:若 step1 enabled 或 config 中有 water_mask_path,
|
||||
则信任 panel/底层的自动推导机制,不拦截。
|
||||
- step14 boundary_shp_path:若 step1 enabled,信任 panel 的自动回填,
|
||||
不拦截。
|
||||
|
||||
关键阻断项(is_critical=True):step1 img_path 缺失。
|
||||
- formula_csv_path:底层完全可选参数,任何情况下都免检。
|
||||
- boundary_path / boundary_shp_path:若 step1 enabled,信任 panel/底层自动推导。
|
||||
"""
|
||||
items: List[MissingItem] = []
|
||||
|
||||
# ── ★ PIPELINE_STEPS step_id → PANEL_REGISTRY step_id(config 的 key)──
|
||||
# ── PIPELINE_STEPS step_id → PANEL_REGISTRY step_id ──
|
||||
_PIPELINE_TO_CONFIG_KEY: Dict[str, str] = {
|
||||
'step1': 'step1',
|
||||
'step2': 'step2',
|
||||
@ -345,64 +340,12 @@ class PreflightDialog(QDialog):
|
||||
'step14': 'step11_map',
|
||||
}
|
||||
|
||||
def _cfg(step_id: str) -> dict:
|
||||
"""用映射表解析 config key,兼容新旧两种 step_id 体系。"""
|
||||
config_key = _PIPELINE_TO_CONFIG_KEY.get(step_id, step_id)
|
||||
return config.get(config_key, {})
|
||||
def _cfg(old_step_id: str) -> dict:
|
||||
"""将旧 PIPELINE_STEPS step_id 翻译为新 PANEL_REGISTRY step_id 后取 config。"""
|
||||
panel_step_id = _PIPELINE_TO_CONFIG_KEY.get(old_step_id, old_step_id)
|
||||
return config.get(panel_step_id, {})
|
||||
|
||||
step1_cfg = _cfg('step1')
|
||||
step1_enabled = step1_cfg.get('enabled', False)
|
||||
|
||||
# ── ★ 构建「动态产物集合」:按 pipeline 顺序收集所有 enabled 步骤的 produces ──
|
||||
dynamically_produced_keys: Set[str] = set()
|
||||
enabled_step_ids: Set[str] = set()
|
||||
|
||||
for step_spec in PIPELINE_STEPS:
|
||||
step_cfg = _cfg(step_spec.step_id)
|
||||
if not step_cfg.get('enabled', True):
|
||||
continue
|
||||
enabled_step_ids.add(step_spec.step_id)
|
||||
dynamically_produced_keys.update(step_spec.produces)
|
||||
|
||||
# ── step1 img_path(阻断性)───────────────────────────────
|
||||
img_path = step1_cfg.get('img_path')
|
||||
if not img_path:
|
||||
items.append(MissingItem(
|
||||
step_id="step1", step_name="水域掩膜",
|
||||
reason="缺少参考影像路径 → 请在「阶段一」中填写「参考影像」",
|
||||
panel_tab_index=0, is_critical=True
|
||||
))
|
||||
elif not os.path.isfile(img_path):
|
||||
items.append(MissingItem(
|
||||
step_id="step1", step_name="水域掩膜",
|
||||
reason=f"参考影像文件不存在:{img_path}",
|
||||
panel_tab_index=0, is_critical=True
|
||||
))
|
||||
|
||||
# ── step5_clean csv_path(纯外部输入,必须手动提供)───────────
|
||||
step4_cfg = _cfg('step4') # 映射到 step5_clean
|
||||
step4_enabled = step4_cfg.get('enabled', True)
|
||||
if step4_enabled:
|
||||
csv_path = step4_cfg.get('csv_path')
|
||||
if not csv_path:
|
||||
items.append(MissingItem(
|
||||
step_id="step5_clean", step_name="数据清洗",
|
||||
reason="请在「数据清洗」中填写「实测水质数据 CSV」",
|
||||
panel_tab_index=3
|
||||
))
|
||||
elif not os.path.isfile(csv_path):
|
||||
items.append(MissingItem(
|
||||
step_id="step5_clean", step_name="数据清洗",
|
||||
reason=f"实测水质数据文件不存在:{csv_path}",
|
||||
panel_tab_index=3
|
||||
))
|
||||
|
||||
# ── step12 formula_csv_path(绝对免检:底层完全可选)────────
|
||||
# formula_csv_path 在底层 CustomRegressionPredictor 中不传即可运行,
|
||||
# 只影响日志输出,不阻断任何功能。此处不做任何检查。
|
||||
|
||||
# ── ★ DAG-aware 检查:遍历 enabled 步骤的 required_input_files ──
|
||||
PURE_EXTERNAL_INPUT_KEYS: Set[str] = {'img_path', 'csv_path'}
|
||||
# ── TAB / STEP NAME 映射(基于 PANEL_REGISTRY step_id)──
|
||||
_TAB_INDEX_MAP: Dict[str, int] = {
|
||||
"step1": 0, "step2": 1, "step3": 2,
|
||||
"step4_sampling": 3, "step5_clean": 4, "step6_feature": 5,
|
||||
@ -419,37 +362,99 @@ class PreflightDialog(QDialog):
|
||||
"step12_viz": "可视化展示", "step13_report": "报告生成",
|
||||
}
|
||||
|
||||
# ── step1 img_path(阻断性)──
|
||||
step1_cfg = _cfg('step1')
|
||||
img_path = step1_cfg.get('img_path')
|
||||
if not img_path:
|
||||
items.append(MissingItem(
|
||||
step_id="step1", step_name="水域掩膜",
|
||||
reason="缺少参考影像路径 → 请在「水域掩膜」中填写「参考影像」",
|
||||
panel_tab_index=0, is_critical=True
|
||||
))
|
||||
elif not os.path.isfile(img_path):
|
||||
items.append(MissingItem(
|
||||
step_id="step1", step_name="水域掩膜",
|
||||
reason=f"参考影像文件不存在:{img_path}",
|
||||
panel_tab_index=0, is_critical=True
|
||||
))
|
||||
|
||||
# ── step5_clean csv_path(纯外部输入,必须手动提供)──
|
||||
step5_clean_cfg = _cfg('step4') # 旧 step4 → 新 step5_clean
|
||||
if step5_clean_cfg.get('enabled', True):
|
||||
csv_path = step5_clean_cfg.get('csv_path')
|
||||
if not csv_path:
|
||||
items.append(MissingItem(
|
||||
step_id="step5_clean", step_name="数据清洗",
|
||||
reason="请在「数据清洗」中填写「实测水质数据 CSV」",
|
||||
panel_tab_index=_TAB_INDEX_MAP.get('step5_clean', 4)
|
||||
))
|
||||
elif not os.path.isfile(csv_path):
|
||||
items.append(MissingItem(
|
||||
step_id="step5_clean", step_name="数据清洗",
|
||||
reason=f"实测水质数据文件不存在:{csv_path}",
|
||||
panel_tab_index=_TAB_INDEX_MAP.get('step5_clean', 4)
|
||||
))
|
||||
|
||||
# ── step4_sampling csv_path(采样点 CSV,2026-07-01 新增缺失检查)──
|
||||
step4_sampling_cfg = _cfg('step10') # 旧 step10 → 新 step4_sampling
|
||||
if step4_sampling_cfg.get('enabled', True):
|
||||
sampling_csv = step4_sampling_cfg.get('csv_path')
|
||||
if not sampling_csv:
|
||||
items.append(MissingItem(
|
||||
step_id="step4_sampling", step_name="采样点布设",
|
||||
reason="请在「采样点布设」中填写「实测水质数据 CSV」",
|
||||
panel_tab_index=_TAB_INDEX_MAP.get('step4_sampling', 3)
|
||||
))
|
||||
elif not os.path.isfile(sampling_csv):
|
||||
items.append(MissingItem(
|
||||
step_id="step4_sampling", step_name="采样点布设",
|
||||
reason=f"实测水质数据文件不存在:{sampling_csv}",
|
||||
panel_tab_index=_TAB_INDEX_MAP.get('step4_sampling', 3)
|
||||
))
|
||||
|
||||
# ── 构建「动态产物集合」:按 pipeline 顺序收集所有 enabled 步骤的 produces ──
|
||||
dynamically_produced_keys: Set[str] = set()
|
||||
enabled_step_ids: Set[str] = set()
|
||||
|
||||
for step_spec in PIPELINE_STEPS:
|
||||
step_cfg = _cfg(step_spec.step_id)
|
||||
if not step_cfg.get('enabled', True):
|
||||
continue
|
||||
enabled_step_ids.add(step_spec.step_id)
|
||||
dynamically_produced_keys.update(step_spec.produces)
|
||||
|
||||
# ── DAG-aware 检查:遍历 enabled 步骤的 required_input_files ──
|
||||
PURE_EXTERNAL_INPUT_KEYS: Set[str] = {'img_path', 'csv_path'}
|
||||
|
||||
for step_spec in PIPELINE_STEPS:
|
||||
if step_spec.step_id not in enabled_step_ids:
|
||||
continue
|
||||
step_cfg = _cfg(step_spec.step_id) # ★ 通过映射表翻译 step_id
|
||||
# ★ 将 PIPELINE_STEPS step_id 翻译为 PANEL_REGISTRY step_id
|
||||
# 将旧 PIPELINE_STEPS step_id 翻译为新 PANEL_REGISTRY step_id
|
||||
panel_step_id = _PIPELINE_TO_CONFIG_KEY.get(step_spec.step_id, step_spec.step_id)
|
||||
tab_idx = _TAB_INDEX_MAP.get(panel_step_id, 0)
|
||||
step_name = _STEP_NAME_MAP.get(panel_step_id, panel_step_id)
|
||||
step_cfg = _cfg(step_spec.step_id)
|
||||
|
||||
for req_key in step_spec.required_input_files:
|
||||
# ★★★ 高优先级硬编码白名单 ★★★
|
||||
# 当检测到需求为边界文件时,只要 step1 有填影像(代表有基础,底层能自动推导),直接放行
|
||||
# 边界文件:只要 step1 有影像输入就信任 panel/底层自动推导
|
||||
if req_key in ('boundary_path', 'boundary_shp_path'):
|
||||
_step1_cfg = _cfg('step1')
|
||||
if _step1_cfg.get('img_path') or _step1_cfg.get('enabled', True):
|
||||
continue # 直接跳过,不判定为缺失
|
||||
if step1_cfg.get('img_path') or step1_cfg.get('enabled', True):
|
||||
continue
|
||||
if req_key in PURE_EXTERNAL_INPUT_KEYS:
|
||||
continue
|
||||
continue # 已在上面显式检查
|
||||
if req_key == 'formula_csv_path':
|
||||
continue # ★ 底层完全可选,赦免
|
||||
continue # 底层完全可选
|
||||
if req_key == 'boundary_path' and panel_step_id == 'step6_feature':
|
||||
continue # ★ step1 执行则 panel/底层自动推导,赦免
|
||||
continue # step1 执行则底层自动推导
|
||||
if req_key == 'boundary_shp_path' and panel_step_id == 'step11_map':
|
||||
continue # ★ step1 执行则 panel 自动回填,赦免
|
||||
continue # step1 执行则 panel 自动回填
|
||||
cfg_val = step_cfg.get(req_key)
|
||||
if cfg_val and os.path.isfile(cfg_val):
|
||||
continue
|
||||
if cfg_val and os.path.isdir(cfg_val):
|
||||
continue
|
||||
if req_key in dynamically_produced_keys:
|
||||
continue # ★ 前置步骤会生成,拓扑预判通过
|
||||
continue # 前置步骤会生成
|
||||
items.append(MissingItem(
|
||||
step_id=panel_step_id,
|
||||
step_name=step_name,
|
||||
|
||||
@ -94,7 +94,7 @@ class VisualizationWorkerThread(QThread):
|
||||
return
|
||||
hyperspectral_path = str(hyperspectral_files[0])
|
||||
csv_files = []
|
||||
processed_dir = wp / "4_processed_data"
|
||||
processed_dir = wp / "5_Data_Cleaning"
|
||||
if processed_dir.exists():
|
||||
csv_files = list(processed_dir.glob("*.csv"))
|
||||
if not csv_files:
|
||||
@ -320,7 +320,7 @@ class VisualizationWorkerThread(QThread):
|
||||
if hyperspectral_files:
|
||||
hyperspectral_path = str(hyperspectral_files[0])
|
||||
csv_files = []
|
||||
processed_dir = wp / "4_processed_data"
|
||||
processed_dir = wp / "5_Data_Cleaning"
|
||||
if processed_dir.exists():
|
||||
csv_files = list(processed_dir.glob("*.csv"))
|
||||
if not csv_files:
|
||||
|
||||
@ -257,11 +257,25 @@ class WorkerThread(QThread):
|
||||
- 整个 run() 方法体包裹在单一 try/except 中
|
||||
- 任何未预期的异常都会被捕获并通过 finished 信号回报主线程
|
||||
- 确保前端永远不会面对"静默死亡"的后台线程
|
||||
|
||||
2026-07-01 GDAL 线程安全加固:
|
||||
- GDAL_NUM_THREADS=1:禁用 GDAL 内部多线程,防止与 QThread 冲突导致 0xC0000005
|
||||
- gdal.AllRegister():在子线程中重新注册 GDAL 驱动
|
||||
"""
|
||||
import os
|
||||
os.environ['GDAL_FILENAME_IS_UTF8'] = 'YES'
|
||||
os.environ['SHAPE_ENCODING'] = 'UTF-8'
|
||||
|
||||
# ★ GDAL 线程安全配置(2026-07-01):
|
||||
# 在子线程中禁用 GDAL 内部多线程,防止多个 QThread 并发调用 GDAL C API 时
|
||||
# 触发 0xC0000005 段错误(GDAL 部分驱动非线程安全)。
|
||||
try:
|
||||
from osgeo import gdal
|
||||
gdal.SetConfigOption('GDAL_NUM_THREADS', '1')
|
||||
gdal.AllRegister()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
mpl_prev = None
|
||||
try:
|
||||
# ★ 终端即时反馈
|
||||
@ -466,11 +480,11 @@ class WorkerThread(QThread):
|
||||
)
|
||||
|
||||
# ── 步骤8_ml_train(ML 建模) ──
|
||||
step7_cfg = config.get('step8_ml_train', {})
|
||||
step7_csv = step7_cfg.get('training_csv_path')
|
||||
if step7_csv and not os.path.isfile(step7_csv):
|
||||
step8_cfg = config.get('step8_ml_train', {})
|
||||
step8_csv = step8_cfg.get('training_csv_path')
|
||||
if step8_csv and not os.path.isfile(step8_csv):
|
||||
errors.append(
|
||||
f"步骤 8_ml_train(ML 建模):训练光谱文件不存在:\n {step7_csv}\n"
|
||||
f"步骤 8_ml_train(ML 建模):训练光谱文件不存在:\n {step8_csv}\n"
|
||||
" → 请确认步骤 5_clean 已成功运行并生成了训练光谱。"
|
||||
)
|
||||
|
||||
|
||||
@ -46,6 +46,7 @@ class WorkspaceInitializer(QObject):
|
||||
super().__init__(parent)
|
||||
self._panel_factory = panel_factory
|
||||
self._work_dir: Optional[str] = None
|
||||
self._previous_work_dir: Optional[str] = None # ★ 2026-07-01:追踪目录变更
|
||||
|
||||
# 工作空间管理器(文件扫描、路径发现)
|
||||
self._workspace_manager = WorkspaceManager()
|
||||
@ -68,6 +69,7 @@ class WorkspaceInitializer(QObject):
|
||||
@work_dir.setter
|
||||
def work_dir(self, value: str):
|
||||
if value and value != self._work_dir:
|
||||
self._previous_work_dir = self._work_dir # ★ 保存旧目录用于清理残留路径
|
||||
self._work_dir = value
|
||||
global_event_bus.publish('WorkspaceChanged', {'work_dir': value})
|
||||
|
||||
@ -137,14 +139,18 @@ class WorkspaceInitializer(QObject):
|
||||
'level': 'info',
|
||||
})
|
||||
|
||||
# 自动填充 step1 输出路径
|
||||
self._auto_fill_output_paths()
|
||||
# 自动填充 step1 输出路径(已禁用:no-op,保留调用点以备日后启用)
|
||||
# self._auto_fill_output_paths()
|
||||
|
||||
def set_work_directory(self):
|
||||
"""手动设置工作目录(菜单触发)。"""
|
||||
"""手动设置工作目录(菜单触发)。
|
||||
|
||||
2026-07-01 修复:切换目录后自动触发完整清理+扫描+回填,
|
||||
确保所有面板的残留旧路径被清除并替换为新目录的路径。
|
||||
"""
|
||||
dir_path = QFileDialog.getExistingDirectory(self.parent(), "选择工作目录")
|
||||
if dir_path:
|
||||
self.work_dir = dir_path
|
||||
self.work_dir = dir_path # ★ setter 中保存 _previous_work_dir + 发布 WorkspaceChanged
|
||||
global_event_bus.publish('LogMessage', {
|
||||
'message': f'工作目录已设置: {dir_path}',
|
||||
'level': 'info',
|
||||
@ -156,6 +162,9 @@ class WorkspaceInitializer(QObject):
|
||||
if panel and hasattr(panel, 'set_work_dir'):
|
||||
panel.set_work_dir(dir_path)
|
||||
|
||||
# ★ 关键修复:切换目录后强制清理旧路径 + 重新扫描回填
|
||||
self.auto_populate_all()
|
||||
|
||||
def open_work_directory(self):
|
||||
"""在资源管理器中打开工作目录(跨平台)。"""
|
||||
import subprocess
|
||||
@ -179,6 +188,7 @@ class WorkspaceInitializer(QObject):
|
||||
"""扫描工作目录并触发事件总线自动填充所有步骤的输入路径。
|
||||
|
||||
流程:
|
||||
0. 若工作目录已变更,先清理指向旧目录的残留输出路径
|
||||
1. WorkspaceManager.scan_work_directory_for_files() 扫描磁盘
|
||||
2. WorkspaceManager 内部自动发布 OutputUpdated 事件
|
||||
3. 各面板通过 DependencySubscriber 自动接收并填充
|
||||
@ -194,6 +204,10 @@ class WorkspaceInitializer(QObject):
|
||||
QMessageBox.warning(self.parent(), "警告", f"工作目录不存在: {work_dir}\n请先设置正确的工作目录。")
|
||||
return
|
||||
|
||||
# ★ 2026-07-01:若目录已变更,先清除指向旧目录的残留路径
|
||||
if self._previous_work_dir and self._previous_work_dir != work_dir:
|
||||
self._clear_stale_output_paths(self._previous_work_dir)
|
||||
|
||||
# WorkspaceManager 扫描 → 内部自动发布 OutputUpdated 事件
|
||||
self._workspace_manager.scan_work_directory_for_files(work_path)
|
||||
|
||||
@ -262,12 +276,65 @@ class WorkspaceInitializer(QObject):
|
||||
# 内部辅助
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
|
||||
def _auto_fill_output_paths(self):
|
||||
"""【已禁用】禁止在工作目录刚选定后瞎拼凑假路径。
|
||||
def _clear_stale_output_paths(self, old_work_dir: str):
|
||||
"""清除所有面板中指向旧工作目录的输出路径(2026-07-01)。
|
||||
|
||||
历史行为:曾经在此处调用 step1_panel.update_work_directory(self._work_dir) 自动回填输出路径。
|
||||
现已清空为 no-op,原因:刚选定目录时任何"自动推断"的子目录都是幽灵路径,
|
||||
当用户切换工作目录时,面板中自动回填的输出路径(如 water_mask_out.dat)
|
||||
仍指向旧目录。由于各 panel 的 update_from_config() 有"非空不覆盖"保护,
|
||||
这些残留路径不会被自动更新。此方法遍历所有已加载面板,
|
||||
清除以旧 work_dir 开头的输出路径,确保后续 auto_populate_all() 能重新填充。
|
||||
|
||||
安全设计:
|
||||
- 仅清除以旧 work_dir 开头的路径(保留用户手动指定的外部路径)
|
||||
- 使用 os.path.normpath 归一化比较,兼容正反斜杠混用
|
||||
"""
|
||||
norm_old = os.path.normpath(old_work_dir).replace('\\', '/')
|
||||
if not norm_old:
|
||||
return
|
||||
|
||||
cleared_count = 0
|
||||
for step_id, panel in self._panel_factory.get_loaded_panels().items():
|
||||
# 遍历面板中常见的输出/路径控件
|
||||
for attr_name in ('output_file', 'output_path', 'output_dir',
|
||||
'models_dir_file', 'sampling_csv_file',
|
||||
'training_csv_file', 'prediction_csv_dir_edit',
|
||||
'boundary_file', 'geotiff_dir_edit'):
|
||||
widget = getattr(panel, attr_name, None)
|
||||
if widget is None:
|
||||
continue
|
||||
# 获取当前路径(兼容 FileSelectWidget 和 QLineEdit)
|
||||
current = ''
|
||||
if hasattr(widget, 'get_path'):
|
||||
current = (widget.get_path() or '').strip()
|
||||
elif hasattr(widget, 'text'):
|
||||
current = (widget.text() or '').strip()
|
||||
if not current:
|
||||
continue
|
||||
# 归一化判断:路径是否指向旧 work_dir
|
||||
norm_current = os.path.normpath(current).replace('\\', '/')
|
||||
if norm_current.startswith(norm_old + '/'):
|
||||
# 清除旧目录残留
|
||||
if hasattr(widget, 'set_path'):
|
||||
widget.set_path('')
|
||||
elif hasattr(widget, 'setText'):
|
||||
widget.setText('')
|
||||
cleared_count += 1
|
||||
|
||||
if cleared_count > 0:
|
||||
global_event_bus.publish('LogMessage', {
|
||||
'message': f'✓ 已清理 {cleared_count} 个指向旧工作目录的残留路径',
|
||||
'level': 'info',
|
||||
})
|
||||
|
||||
def _auto_fill_output_paths(self):
|
||||
"""【已禁用 — 2026-06-30 起永久禁用】
|
||||
|
||||
历史行为:曾经在此处调用 step1_panel.update_work_directory(self._work_dir)
|
||||
自动回填输出路径。现已清空为 no-op。
|
||||
原因:刚选定目录时任何"自动推断"的子目录都是幽灵路径,
|
||||
会污染后续面板的 input field,让用户误以为已经填好了实际却指向不存在的目录。
|
||||
真实填充时机交由面板自身的 update_from_config + 用户手动指定。
|
||||
|
||||
调用点已在 run() 中注释掉,此方法仅保留定义供外部存档引用。
|
||||
"""
|
||||
pass
|
||||
|
||||
Reference in New Issue
Block a user