From ffcdee68a5b916104615949b0aedb4ec7c6f1b09 Mon Sep 17 00:00:00 2001 From: 11cookies11 Date: Sun, 8 Mar 2026 18:31:36 +0800 Subject: [PATCH 01/15] =?UTF-8?q?=EF=BB=BFfix(ui):=20add=20webengine=20bla?= =?UTF-8?q?ckscreen=20recovery=20and=20simplify=20scripts/sidebar=20panels?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [中文] - 变更内容: - 新增 WebEngine 渲染进程异常监听,出现黑屏类崩溃时自动以安全渲染参数重启一次(GPU 降级) - 启动日志增加 WebEngine flags 输出,便于排障 - 删除自动脚本页“变量监控”卡片(含无用绑定清理) - 删除侧边栏底部 DevUser_01/管理员工作区展示块 - 更新 where 进度文件 - 影响范围: - app/main_web.py - ui/desktop/web_window.py - ui/frontend/src/components/ScriptsView.vue - ui/frontend/src/App.vue - .where-agent-progress.md - 兼容性/行为变化: - UI 布局变化:脚本页不再显示变量监控卡片,侧边栏不再显示开发者用户块 - 运行行为增强:WebEngine 渲染进程异常时自动触发一次安全模式重启 - 依赖/环境: - 无新增依赖;可通过环境变量 PROTOFLOW_DISABLE_GPU_RECOVERY=1 禁用自动恢复 - 验证: - 代码语法检查通过:python -m py_compile app/main_web.py ui/desktop/web_window.py - 手工检查差异:仅包含上述文件改动 [English] - Changes: - Added WebEngine render-process termination handling and one-time auto-restart with safe rendering flags (GPU fallback) for blackscreen-class failures. - Added startup logging of effective WebEngine flags for troubleshooting. - Removed the Script page "Variable Monitor" card and cleaned unused bindings. - Removed the sidebar bottom DevUser_01/workspace block. - Updated where progress file. - Impact: - app/main_web.py - ui/desktop/web_window.py - ui/frontend/src/components/ScriptsView.vue - ui/frontend/src/App.vue - .where-agent-progress.md - Compatibility/Behavior changes: - UI layout update: Script page no longer shows the variable monitor card; sidebar no longer shows the developer user block. - Runtime resilience update: app performs one-time safe-mode restart when WebEngine render process terminates. - Dependencies/Environment: - No new dependencies; auto recovery can be disabled via PROTOFLOW_DISABLE_GPU_RECOVERY=1. - Verification: - Syntax check passed: python -m py_compile app/main_web.py ui/desktop/web_window.py - Manual diff review confirms scope-limited file changes. Refs: - N/A --- .where-agent-progress.md | 9 +++-- app/main_web.py | 4 +++ ui/desktop/web_window.py | 40 ++++++++++++++++++++++ ui/frontend/src/App.vue | 8 ----- ui/frontend/src/components/ScriptsView.vue | 25 -------------- 5 files changed, 48 insertions(+), 38 deletions(-) diff --git a/.where-agent-progress.md b/.where-agent-progress.md index cf92b3a..6f932df 100644 --- a/.where-agent-progress.md +++ b/.where-agent-progress.md @@ -1,5 +1,4 @@ -# Plan: ProtoFlow English User Guide Sync -- [x] Align structure with Chinese guide (learn-first flow) -- [x] Rewrite docs/USER_GUIDE_EN.md with current software behavior -- [x] Verify feature status/commands against implementation -- [x] Finalize delivery notes +# Plan: Scripts 页面变量监控卡片下线 +- [x] 定位并移除 ScriptsView 中的变量监控卡片 +- [x] 清理组件内未使用绑定(scriptVariables/refreshScriptVariables) +- [x] 校验变更为最小差异且无编码污染 diff --git a/app/main_web.py b/app/main_web.py index 81af138..ddd899d 100644 --- a/app/main_web.py +++ b/app/main_web.py @@ -199,6 +199,10 @@ def main() -> None: flags = _select_webengine_flags() if flags: os.environ.setdefault("QTWEBENGINE_CHROMIUM_FLAGS", flags) + logging.getLogger("main_web").info( + "WebEngine flags: %s", + os.environ.get("QTWEBENGINE_CHROMIUM_FLAGS", ""), + ) bus = EventBus() comm = CommunicationManager(bus) proxy_enabled = _proxy_monitor_enabled() diff --git a/ui/desktop/web_window.py b/ui/desktop/web_window.py index e574b3d..4c3b0c0 100644 --- a/ui/desktop/web_window.py +++ b/ui/desktop/web_window.py @@ -4,6 +4,7 @@ import ctypes import logging import os +import subprocess import sys try: @@ -60,6 +61,8 @@ def javaScriptConsoleMessage(self, level, message, line_number, source_id): # t class WebWindow(QMainWindow): """Minimal WebEngine host window for the new web UI.""" + _GPU_SAFE_FLAGS = "--disable-features=DirectComposition --disable-gpu --use-angle=swiftshader" + def __init__(self, bus=None, comm=None, proxy_manager=None, proxy_monitor_enabled: bool = True) -> None: super().__init__() self.setWindowTitle("ProtoFlow Web UI") @@ -80,8 +83,10 @@ def __init__(self, bus=None, comm=None, proxy_manager=None, proxy_monitor_enable view = QWebEngineView(self) page = LoggingWebPage(view) view.setPage(page) + page.renderProcessTerminated.connect(self._on_render_process_terminated) self._view = view self._stabilize_pending = False + self._gpu_recovery_done = os.environ.get("PROTOFLOW_WEBENGINE_RECOVERY", "0") == "1" self.setCentralWidget(view) channel = QWebChannel(view) @@ -481,3 +486,38 @@ def _init_titlebar(self): def showEvent(self, event): super().showEvent(event) self._init_titlebar() + + def _on_render_process_terminated(self, termination_status, exit_code) -> None: + logger = logging.getLogger("web_window") + logger.error( + "WebEngine render process terminated: status=%s exit_code=%s", + termination_status, + exit_code, + ) + if self._gpu_recovery_done: + logger.error("GPU fallback already attempted; skip auto-restart") + return + if os.environ.get("PROTOFLOW_DISABLE_GPU_RECOVERY", "0") in {"1", "true", "TRUE"}: + logger.warning("GPU recovery disabled by PROTOFLOW_DISABLE_GPU_RECOVERY") + return + self._gpu_recovery_done = True + self._restart_with_gpu_safe_mode() + + def _restart_with_gpu_safe_mode(self) -> None: + logger = logging.getLogger("web_window") + env = dict(os.environ) + env["PROTOFLOW_WEBENGINE_RECOVERY"] = "1" + env["PROTOFLOW_WEBENGINE_FLAGS"] = self._GPU_SAFE_FLAGS + + if getattr(sys, "frozen", False): + cmd = [sys.executable, *sys.argv[1:]] + else: + cmd = [sys.executable, *sys.argv] + try: + subprocess.Popen(cmd, env=env, cwd=str(Path.cwd())) + logger.warning("Restarting app with safe WebEngine flags: %s", self._GPU_SAFE_FLAGS) + except Exception: + logger.exception("Failed to restart app in GPU safe mode") + return + + QTimer.singleShot(150, self.close) diff --git a/ui/frontend/src/App.vue b/ui/frontend/src/App.vue index 984c561..d786ea6 100644 --- a/ui/frontend/src/App.vue +++ b/ui/frontend/src/App.vue @@ -126,7 +126,6 @@ const uiLabels = computed(() => ({ scripts: t('nav.scripts'), protocols: t('nav.protocols'), settings: t('nav.settings'), - workspace: t('nav.workspace'), })) const channelTab = ref('all') const uiRuntime = useUiRuntimeStore() @@ -1132,13 +1131,6 @@ const { startBridgeBootstrap, disposeBridgeBootstrap } = useBridgeBootstrap({ {{ uiLabels.settings }} -
diff --git a/ui/frontend/src/components/ScriptsView.vue b/ui/frontend/src/components/ScriptsView.vue index 3b2c675..7fa7e44 100644 --- a/ui/frontend/src/components/ScriptsView.vue +++ b/ui/frontend/src/components/ScriptsView.vue @@ -34,8 +34,6 @@ const { scriptProgress, scriptElapsedLabel, scriptErrorCount, - scriptVariables, - refreshScriptVariables, clearScriptLogs, scrollScriptLogsToBottom, renderedScriptLogs, @@ -139,29 +137,6 @@ const { -
-
{{ tr('变量监控') }} -
- - - - - - - - - - - - - - - - - - -
{{ tr('变量名') }}{{ tr('当前值') }}
{{ item.name }}{{ item.value || '--' }}
--
-
From 156d30e6059bdceb85944f670bbfa8be8313f50e Mon Sep 17 00:00:00 2001 From: 11cookies11 Date: Sun, 8 Mar 2026 18:56:07 +0800 Subject: [PATCH 02/15] =?UTF-8?q?=EF=BB=BFfix(ui):=20integrate=20real=20pl?= =?UTF-8?q?ugin/protocol=20lists=20and=20refine=20settings/scripts=20layou?= =?UTF-8?q?t?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [中文] - 变更内容: - 设置页插件区域改为真实数据接入:新增后端桥接插件列表接口,前端支持刷新并展示真实插件项 - 设置页新增协议包列表展示(接入 list_protocols),与插件列表同页可见 - 为插件/协议包列表增加筛选能力(状态/类别) - 修复设置页目录区域布局问题(工作目录/插件目录展示与按钮对齐) - 删除脚本页变量监控后,补齐右侧布局填充,避免大面积留白 - 保留并提交此前 WebEngine 黑屏自动恢复与日志增强改动 - 更新 where 进度文件 - 影响范围: - app/main_web.py - ui/desktop/web_window.py - ui/desktop/web_bridge.py - ui/frontend/src/App.vue - ui/frontend/src/components/SettingsPanels.vue - ui/frontend/src/composables/useBridgeBootstrap.ts - ui/frontend/src/composables/useBridgeBootstrap.test.ts - ui/frontend/src/style.css - .where-agent-progress.md - 兼容性/行为变化: - 设置页由静态示例展示改为真实插件/协议包数据展示 - UI 布局有调整(目录区、脚本页右侧、筛选条) - WebEngine 异常时触发一次安全模式自动重启(可通过环境变量关闭) - 依赖/环境: - 无新增依赖 - 可选环境变量:PROTOFLOW_DISABLE_GPU_RECOVERY=1 - 验证: - 前端测试通过: - npm test -- --run src/composables/useBridgeBootstrap.test.ts src/components/SettingsPanels.test.ts - npm test -- --run src/components/SettingsPanels.test.ts - Python 语法检查通过: - python -m py_compile app/main_web.py ui/desktop/web_window.py ui/desktop/web_bridge.py app/plugin_manager.py [English] - Changes: - Reworked Settings plugin section to use real runtime data via new backend bridge plugin-list API; frontend refresh now renders actual plugin entries. - Added protocol-package list rendering in Settings (wired to list_protocols) so plugins and protocol packages are both visible. - Added filtering for plugin/protocol lists (status/category). - Fixed Settings directory-area layout (workspace/plugin directory alignment and controls). - Filled right-side Scripts layout after removing variable monitor to avoid large blank area. - Kept and included prior WebEngine blackscreen auto-recovery and startup flag logging changes. - Updated where progress file. - Impact: - app/main_web.py - ui/desktop/web_window.py - ui/desktop/web_bridge.py - ui/frontend/src/App.vue - ui/frontend/src/components/SettingsPanels.vue - ui/frontend/src/composables/useBridgeBootstrap.ts - ui/frontend/src/composables/useBridgeBootstrap.test.ts - ui/frontend/src/style.css - .where-agent-progress.md - Compatibility/Behavior changes: - Settings view now shows real plugin/protocol-package data instead of static placeholders. - UI layout updates in directory section, scripts right column, and list filter bars. - One-time safe-mode restart is triggered on WebEngine render-process termination (configurable). - Dependencies/Environment: - No new dependencies. - Optional env var: PROTOFLOW_DISABLE_GPU_RECOVERY=1 - Verification: - Frontend tests passed: - npm test -- --run src/composables/useBridgeBootstrap.test.ts src/components/SettingsPanels.test.ts - npm test -- --run src/components/SettingsPanels.test.ts - Python syntax checks passed: - python -m py_compile app/main_web.py ui/desktop/web_window.py ui/desktop/web_bridge.py app/plugin_manager.py Refs: - N/A --- .where-agent-progress.md | 9 +- app/main_web.py | 5 +- ui/desktop/web_bridge.py | 47 ++++++ ui/desktop/web_window.py | 3 +- ui/frontend/src/App.vue | 59 +++++++ ui/frontend/src/components/SettingsPanels.vue | 152 +++++++++++++++--- .../composables/useBridgeBootstrap.test.ts | 6 + .../src/composables/useBridgeBootstrap.ts | 2 + ui/frontend/src/style.css | 73 ++++++++- 9 files changed, 319 insertions(+), 37 deletions(-) diff --git a/.where-agent-progress.md b/.where-agent-progress.md index 6f932df..9db7ea3 100644 --- a/.where-agent-progress.md +++ b/.where-agent-progress.md @@ -1,4 +1,5 @@ -# Plan: Scripts 页面变量监控卡片下线 -- [x] 定位并移除 ScriptsView 中的变量监控卡片 -- [x] 清理组件内未使用绑定(scriptVariables/refreshScriptVariables) -- [x] 校验变更为最小差异且无编码污染 +# Plan: 设置页插件与协议包筛选增强 +- [x] 为插件列表增加状态筛选(全部/已启用/可用/异常) +- [x] 为协议包列表增加分类筛选(全部/Modbus/TCP/自定义) +- [x] 补充筛选按钮样式并保持响应式 +- [x] 通过 SettingsPanels 组件测试 diff --git a/app/main_web.py b/app/main_web.py index ddd899d..4ef957b 100644 --- a/app/main_web.py +++ b/app/main_web.py @@ -210,8 +210,8 @@ def main() -> None: logging.getLogger("main_web").info("Proxy monitor enabled: %s", proxy_enabled) protocol = ProtocolLoader(bus) packet_engine = PacketAnalysisEngine(bus) - plugins = PluginManager(bus, protocol=protocol) - plugins.load_all() + plugin_manager = PluginManager(bus, protocol=protocol) + plugin_manager.load_all() app = QApplication.instance() or QApplication(sys.argv) _shutdown_done = {"value": False} @@ -232,6 +232,7 @@ def _shutdown_proxy() -> None: window = WebWindow( bus=bus, comm=comm, + plugin_manager=plugin_manager, proxy_manager=proxy_manager, proxy_monitor_enabled=proxy_enabled, ) diff --git a/ui/desktop/web_bridge.py b/ui/desktop/web_bridge.py index ccb5627..a0cccaf 100644 --- a/ui/desktop/web_bridge.py +++ b/ui/desktop/web_bridge.py @@ -62,6 +62,7 @@ def __init__( self, bus=None, comm=None, + plugin_manager=None, window=None, proxy_manager=None, proxy_monitor_enabled: bool = True, @@ -70,6 +71,7 @@ def __init__( self._logger = logging.getLogger("web_bridge") self._bus = bus self._comm = comm + self._plugin_manager = plugin_manager self._window = window self._proxy_manager = proxy_manager self._proxy_monitor_enabled = bool(proxy_monitor_enabled) @@ -274,6 +276,51 @@ def delete_protocol(self, protocol_id: str) -> bool: def load_settings(self) -> Dict[str, Any]: return self._load_settings() + @Slot(result="QVariant") + def list_plugins(self) -> Dict[str, Any]: + manager = self._plugin_manager + if manager is None: + return {"directory": "", "items": []} + + plugin_dir = Path(getattr(manager, "plugin_dir", Path.cwd() / "plugins")) + loaded = set() + try: + loaded = set(manager.list_plugins()) + except Exception: + loaded = set() + + items: List[Dict[str, Any]] = [] + try: + for path in sorted(plugin_dir.glob("*.py")): + if path.name.startswith("_"): + continue + name = path.stem + item = { + "id": name, + "name": name, + "version": "", + "status": "enabled" if name in loaded else "available", + "path": str(path), + } + module = getattr(manager, "_plugins", {}).get(name) + if module is not None: + plugin_name = getattr(module, "PLUGIN_NAME", None) + if isinstance(plugin_name, str) and plugin_name.strip(): + item["name"] = plugin_name.strip() + version = getattr(module, "__version__", "") + if isinstance(version, str): + item["version"] = version + items.append(item) + except Exception as exc: + self._logger.exception("Failed to enumerate plugins: %s", exc) + return {"directory": str(plugin_dir), "items": items} + + @Slot(result="QVariant") + def refresh_plugins(self) -> Dict[str, Any]: + # Refresh is intentionally enumeration-only to avoid duplicate plugin + # event subscriptions caused by repeated register() calls. + return self.list_plugins() + @Slot(result="QVariant") def get_feature_flags(self) -> Dict[str, Any]: return { diff --git a/ui/desktop/web_window.py b/ui/desktop/web_window.py index 4c3b0c0..7b2c20b 100644 --- a/ui/desktop/web_window.py +++ b/ui/desktop/web_window.py @@ -63,7 +63,7 @@ class WebWindow(QMainWindow): _GPU_SAFE_FLAGS = "--disable-features=DirectComposition --disable-gpu --use-angle=swiftshader" - def __init__(self, bus=None, comm=None, proxy_manager=None, proxy_monitor_enabled: bool = True) -> None: + def __init__(self, bus=None, comm=None, plugin_manager=None, proxy_manager=None, proxy_monitor_enabled: bool = True) -> None: super().__init__() self.setWindowTitle("ProtoFlow Web UI") self._apply_initial_geometry() @@ -93,6 +93,7 @@ def __init__(self, bus=None, comm=None, proxy_manager=None, proxy_monitor_enable self.bridge = WebBridge( bus=bus, comm=comm, + plugin_manager=plugin_manager, window=self, proxy_manager=proxy_manager, proxy_monitor_enabled=proxy_monitor_enabled, diff --git a/ui/frontend/src/App.vue b/ui/frontend/src/App.vue index d786ea6..2800294 100644 --- a/ui/frontend/src/App.vue +++ b/ui/frontend/src/App.vue @@ -108,6 +108,10 @@ const autoConnectOnStart = ref(uiDefaults.autoConnectOnStart) const settingsSaving = ref(false) const settingsSnapshot = ref(null) const settingsTab = ref('general') +const pluginDirectory = ref('') +const pluginItems = ref([]) +const protocolItems = ref([]) +const pluginsRefreshing = ref(false) const { translations, supportedLanguages, DEFAULT_LANGUAGE } = i18nCore const t = (key, fallback = '') => { @@ -486,6 +490,46 @@ function setSettingsTab(tab) { settingsTab.value = tab } +function refreshPlugins() { + if (!bridge.value) return + pluginsRefreshing.value = true + protocolItems.value = [] + const done = () => { + pluginsRefreshing.value = false + } + if (bridge.value.refresh_plugins) { + withResult(bridge.value.refresh_plugins(), (payload) => { + pluginDirectory.value = String(payload?.directory || '') + pluginItems.value = Array.isArray(payload?.items) ? payload.items : [] + if (bridge.value.list_protocols) { + withResult(bridge.value.list_protocols(), (protocols) => { + protocolItems.value = Array.isArray(protocols) ? protocols : [] + done() + }) + } else { + done() + } + }) + return + } + if (bridge.value.list_plugins) { + withResult(bridge.value.list_plugins(), (payload) => { + pluginDirectory.value = String(payload?.directory || '') + pluginItems.value = Array.isArray(payload?.items) ? payload.items : [] + if (bridge.value.list_protocols) { + withResult(bridge.value.list_protocols(), (protocols) => { + protocolItems.value = Array.isArray(protocols) ? protocols : [] + done() + }) + } else { + done() + } + }) + return + } + done() +} + const manualViewBindings = { connectionInfo, isConnected, @@ -909,6 +953,15 @@ watch( } ) +watch( + () => settingsTab.value, + (value) => { + if (value === 'plugins') { + refreshPlugins() + } + } +) + watch( () => yamlCollapsed.value, (collapsed) => { @@ -1038,6 +1091,7 @@ const { startBridgeBootstrap, disposeBridgeBootstrap } = useBridgeBootstrap({ refreshPorts, refreshChannels, refreshProtocols, + refreshPlugins, loadSettings, }) @@ -1161,10 +1215,15 @@ const { startBridgeBootstrap, disposeBridgeBootstrap } = useBridgeBootstrap({ :ui-theme="uiTheme" :auto-connect-on-start="autoConnectOnStart" :dsl-workspace-path="dslWorkspacePath" + :plugin-directory="pluginDirectory" + :plugin-items="pluginItems" + :protocol-items="protocolItems" + :plugins-refreshing="pluginsRefreshing" :language-options="languageOptions" :theme-options="themeOptions" @set-tab="setSettingsTab" @choose-dsl-workspace="chooseDslWorkspace" + @refresh-plugins="refreshPlugins" @update:ui-language="uiLanguage = $event" @update:ui-theme="uiTheme = $event" @update:auto-connect-on-start="autoConnectOnStart = $event" diff --git a/ui/frontend/src/components/SettingsPanels.vue b/ui/frontend/src/components/SettingsPanels.vue index 179c890..5d1ae95 100644 --- a/ui/frontend/src/components/SettingsPanels.vue +++ b/ui/frontend/src/components/SettingsPanels.vue @@ -1,11 +1,11 @@ -