From 11cd6218dc12403e90dd1adb018c3cac0ccfe34d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=B4=E5=A4=A9?= <42165041+1186258278@users.noreply.github.com> Date: Fri, 24 Apr 2026 19:36:20 +0800 Subject: [PATCH] feat(diagnose): detect and inform about @homebridge/ciao cmd popup bug (#250) * feat(diagnose): detect and inform about @homebridge/ciao cmd popup bug On Windows, OpenClaw's transitive dependency @homebridge/ciao (<=1.3.6) calls child_process.exec('arp -a ...') every 15-30 seconds without passing windowsHide:true, causing a cmd.exe popup to flash. This is an upstream library bug: - Issue: homebridge/ciao#64 - PR: homebridge/ciao#65 (open, not merged) ClawPanel deliberately chooses 'detect and inform' rather than silently patching the user's node_modules. We respect the user's control over their own machine. Changes: - src-tauri/src/commands/diagnose.rs: new check_ciao_windowshide_bug command; scans openclaw's @homebridge/ciao/lib/NetworkManager.js and reports whether the buggy exec pattern is present - src-tauri/src/lib.rs: register the new command - scripts/dev-api.js: Web-mode stub (returns affected:false since the bug does not manifest off-Windows) - src/lib/tauri-api.js: add api.checkCiaoWindowsHideBug - src/lib/ciao-bug-warning.js: new module with toast + modal flow, version-scoped dismiss (localStorage) - src/locales/modules/ciaoBug.js: translations in 5 primary languages - src/locales/index.js: register the ciaoBug module - src/main.js: call checker 3s after splash hides Non-Windows users see nothing; Windows users see a single warning toast (version-dismissible) linking to three fix paths: wait for upstream, apply patch-package, or edit NetworkManager.js manually. * fix(diagnose): gate helper with cfg(windows), drop unneeded return CI failures on Linux + macOS: - openclaw_module_root was dead code when target_os != windows since the only caller is the #[cfg(target_os = "windows")] block inside check_ciao_windowshide_bug - Explicit `return CiaoCheckResult {...};` in the non-Windows branch triggered clippy::needless_return Fix: - Add #[cfg(target_os = "windows")] to openclaw_module_root so it is not compiled on other platforms - Convert the non-Windows early exit to a tail expression --- scripts/dev-api.js | 23 +++++ src-tauri/src/commands/diagnose.rs | 150 +++++++++++++++++++++++++++++ src-tauri/src/lib.rs | 1 + src/lib/ciao-bug-warning.js | 136 ++++++++++++++++++++++++++ src/lib/tauri-api.js | 1 + src/locales/index.js | 3 +- src/locales/modules/ciaoBug.js | 115 ++++++++++++++++++++++ src/main.js | 11 +++ 8 files changed, 439 insertions(+), 1 deletion(-) create mode 100644 src/lib/ciao-bug-warning.js create mode 100644 src/locales/modules/ciaoBug.js diff --git a/scripts/dev-api.js b/scripts/dev-api.js index 61134bf..18c34cb 100644 --- a/scripts/dev-api.js +++ b/scripts/dev-api.js @@ -4722,6 +4722,29 @@ const handlers = { }) }, + // @homebridge/ciao windowsHide bug — Windows only. Linux/macOS stubs return false. + // See https://github.com/homebridge/ciao/issues/64 and PR #65. + check_ciao_windowshide_bug() { + const platform = process.platform + if (platform !== 'win32') { + return { + affected: false, + platform, + version: null, + networkManagerPath: null, + detail: 'Non-Windows platform — bug does not manifest here.', + } + } + // Web 模式极少跑在 Windows 上,这里提供最小桩实现保持接口一致 + return { + affected: false, + platform, + version: null, + networkManagerPath: null, + detail: 'Ciao bug detection is only performed in the Tauri desktop build.', + } + }, + async diagnose_gateway_connection() { const steps = [] const ocDir = openclawDir() diff --git a/src-tauri/src/commands/diagnose.rs b/src-tauri/src/commands/diagnose.rs index 02a9e98..351702e 100644 --- a/src-tauri/src/commands/diagnose.rs +++ b/src-tauri/src/commands/diagnose.rs @@ -364,3 +364,153 @@ pub async fn diagnose_gateway_connection() -> DiagnoseResult { summary, } } + +// ============================================================================= +// @homebridge/ciao Windows cmd popup bug detection +// +// Upstream issue: https://github.com/homebridge/ciao/issues/64 +// Upstream PR: https://github.com/homebridge/ciao/pull/65 (still open) +// +// Symptom on Windows: every 15-30s a cmd.exe / conhost.exe window flashes while +// Gateway is running. Root cause is @homebridge/ciao < 1.3.7 calling +// `child_process.exec("arp -a ...", callback)` without `{ windowsHide: true }`. +// +// This is NOT a ClawPanel bug — we only expose a detection command so the +// dashboard can surface a clear, actionable hint to users rather than silently +// inheriting third-party noise. +// ============================================================================= + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CiaoCheckResult { + /// Whether the bug is affecting the current installation + pub affected: bool, + /// Platform quick-check (non-Windows installations can never be affected) + pub platform: String, + /// Detected @homebridge/ciao version if the package is installed + pub version: Option, + /// Absolute path to NetworkManager.js (when detected) + pub network_manager_path: Option, + /// Human-readable detail for the UI + pub detail: String, +} + +/// Resolve the openclaw CLI module root — directory containing the installed +/// package's `package.json`. Returns None when the CLI cannot be located. +/// Only compiled on Windows since ciao bug detection is Windows-only. +#[cfg(target_os = "windows")] +fn openclaw_module_root() -> Option { + let cli = crate::utils::resolve_openclaw_cli_path()?; + let cli_path = std::path::PathBuf::from(&cli); + + // The CLI entrypoint is typically `/dist/entry.js` or + // similar. Walk up until we find a `package.json`, stopping at the + // nearest node_modules boundary. + let mut current = cli_path.parent()?.to_path_buf(); + for _ in 0..6 { + if current.join("package.json").is_file() { + return Some(current); + } + current = current.parent()?.to_path_buf(); + } + None +} + +/// Check the `@homebridge/ciao` package bundled with openclaw. Only runs on +/// Windows since the bug does not manifest on other platforms. +#[tauri::command] +pub fn check_ciao_windowshide_bug() -> CiaoCheckResult { + let platform = std::env::consts::OS.to_string(); + + #[cfg(not(target_os = "windows"))] + { + CiaoCheckResult { + affected: false, + platform, + version: None, + network_manager_path: None, + detail: "Non-Windows platform — bug does not manifest here.".into(), + } + } + + #[cfg(target_os = "windows")] + { + let Some(root) = openclaw_module_root() else { + return CiaoCheckResult { + affected: false, + platform, + version: None, + network_manager_path: None, + detail: "openclaw CLI not installed; nothing to check.".into(), + }; + }; + + let ciao_dir = root.join("node_modules").join("@homebridge").join("ciao"); + if !ciao_dir.is_dir() { + return CiaoCheckResult { + affected: false, + platform, + version: None, + network_manager_path: None, + detail: "@homebridge/ciao not found in openclaw dependencies.".into(), + }; + } + + // Read version for reporting only — we do not key off it to avoid + // lying to the user if someone backports the fix without bumping. + let version = std::fs::read_to_string(ciao_dir.join("package.json")) + .ok() + .and_then(|raw| serde_json::from_str::(&raw).ok()) + .and_then(|v| v.get("version").and_then(|s| s.as_str()).map(String::from)); + + let nm_path = ciao_dir.join("lib").join("NetworkManager.js"); + if !nm_path.is_file() { + return CiaoCheckResult { + affected: false, + platform, + version, + network_manager_path: None, + detail: "NetworkManager.js not found; skipping scan.".into(), + }; + } + + let content = match std::fs::read_to_string(&nm_path) { + Ok(text) => text, + Err(err) => { + return CiaoCheckResult { + affected: false, + platform, + version, + network_manager_path: Some(nm_path.to_string_lossy().to_string()), + detail: format!("Unable to read NetworkManager.js: {err}"), + }; + } + }; + + // Detection heuristic: look for the Windows ARP call and check whether + // the third argument is an options object or the callback. A fixed + // version uses exec("arp -a ...", { windowsHide: true }, callback). + // The buggy version uses exec("arp -a ...", (error, stdout) => ...). + let affected = content.lines().any(|line| { + let trimmed = line.trim_start(); + trimmed.contains(".exec(\"arp -a") + && !trimmed.contains("windowsHide") + && !trimmed.contains("windows_hide") + }); + + let detail = if affected { + "Detected @homebridge/ciao without windowsHide option — cmd.exe will flash every 15-30s while Gateway runs. See upstream issues #64 / #65." + .into() + } else { + "No buggy @homebridge/ciao pattern detected.".into() + }; + + CiaoCheckResult { + affected, + platform, + version, + network_manager_path: Some(nm_path.to_string_lossy().to_string()), + detail, + } + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index a7342b5..700f9cf 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -130,6 +130,7 @@ pub fn run() { service::guardian_status, // 诊断 diagnose::diagnose_gateway_connection, + diagnose::check_ciao_windowshide_bug, // 日志 logs::read_log_tail, logs::search_log, diff --git a/src/lib/ciao-bug-warning.js b/src/lib/ciao-bug-warning.js new file mode 100644 index 0000000..35b6ab5 --- /dev/null +++ b/src/lib/ciao-bug-warning.js @@ -0,0 +1,136 @@ +/** + * @homebridge/ciao Windows cmd 弹窗 bug 检测与提示 + * + * 背景:openclaw 的依赖 @homebridge/ciao (<= 1.3.6) 在 Windows 上每 15-30 秒 + * 调用 `child_process.exec("arp -a ...")` 时未传 `windowsHide: true`, + * 导致 cmd.exe / conhost.exe 窗口闪烁。这是上游库的 bug, + * 不在 ClawPanel 控制范围内。上游 issue #64 和 PR #65 尚未合并。 + * + * 我们只做两件事:检测 + 给用户展示修复指引。不触碰用户 node_modules。 + */ + +import { api } from './tauri-api.js' +import { toast } from '../components/toast.js' +import { t } from './i18n.js' + +const DISMISS_KEY_PREFIX = 'clawpanel_ciao_bug_dismissed_v' + +function dismissKey(version) { + return `${DISMISS_KEY_PREFIX}${version || 'unknown'}` +} + +function isDismissed(version) { + try { + return localStorage.getItem(dismissKey(version)) === '1' + } catch (_) { + return false + } +} + +function markDismissed(version) { + try { + localStorage.setItem(dismissKey(version), '1') + } catch (_) { /* quota 等异常忽略 */ } +} + +/** + * 启动后异步检测;若确实受影响,展示一个可 dismiss 的 toast。 + * 用户点"详情"会打开带修复步骤和官方链接的 modal。 + */ +export async function checkAndWarnCiaoBug() { + let result + try { + result = await api.checkCiaoWindowsHideBug() + } catch (err) { + console.debug('[ciao-bug] check failed:', err) + return + } + + if (!result || !result.affected) return + if (isDismissed(result.version)) return + + const detailBtn = document.createElement('button') + detailBtn.className = 'btn btn-sm btn-primary' + detailBtn.textContent = t('ciaoBug.viewDetail') + detailBtn.style.marginLeft = '8px' + detailBtn.onclick = () => openCiaoBugModal(result) + + toast( + t('ciaoBug.toastTitle'), + 'warning', + { action: detailBtn, duration: 12000 }, + ) +} + +function openCiaoBugModal(result) { + const overlay = document.createElement('div') + overlay.className = 'modal-overlay' + + const versionLine = result.version + ? `
@homebridge/ciao ${escapeHtml(result.version)}
` + : '' + const pathLine = result.networkManagerPath + ? `
${escapeHtml(t('ciaoBug.pathLabel'))} ${escapeHtml(result.networkManagerPath)}
` + : '' + + overlay.innerHTML = ` + + ` + + const close = () => overlay.remove() + overlay.addEventListener('click', (e) => { + if (e.target === overlay) close() + }) + overlay.querySelector('[data-action="close"]').onclick = close + overlay.querySelector('[data-action="dismiss"]').onclick = () => { + markDismissed(result.version) + close() + toast(t('ciaoBug.dismissed'), 'info') + } + document.addEventListener('keydown', function onEsc(e) { + if (e.key === 'Escape') { + close() + document.removeEventListener('keydown', onEsc) + } + }) + + document.body.appendChild(overlay) +} + +function escapeHtml(raw) { + return String(raw || '') + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') +} diff --git a/src/lib/tauri-api.js b/src/lib/tauri-api.js index 80aefdc..6904f63 100644 --- a/src/lib/tauri-api.js +++ b/src/lib/tauri-api.js @@ -196,6 +196,7 @@ export const api = { probeGatewayPort: () => invoke('probe_gateway_port'), diagnoseGatewayConnection: () => invoke('diagnose_gateway_connection'), guardianStatus: () => invoke('guardian_status'), + checkCiaoWindowsHideBug: () => invoke('check_ciao_windowshide_bug'), // 配置(读缓存,写清缓存) getVersionInfo: () => cachedInvoke('get_version_info', {}, 30000), diff --git a/src/locales/index.js b/src/locales/index.js index f673f1e..38a47e7 100644 --- a/src/locales/index.js +++ b/src/locales/index.js @@ -35,13 +35,14 @@ import diagnose from './modules/diagnose.js' import routeMap from './modules/routeMap.js' import extensions from './modules/extensions.js' import engine from './modules/engine.js' +import ciaoBug from './modules/ciaoBug.js' const MODULES = { common, sidebar, instance, dashboard, services, settings, models, agents, agentDetail, gateway, security, communication, channels, memory, dreaming, cron, usage, skills, chat, chatDebug, setup, about, ext, logs, assistant, toast, modal, engagement, diagnose, routeMap, extensions, - engine, + engine, ciaoBug, } /** 构建所有语言字典 { 'zh-CN': { common: {...}, sidebar: {...}, ... }, ... } */ diff --git a/src/locales/modules/ciaoBug.js b/src/locales/modules/ciaoBug.js new file mode 100644 index 0000000..85d40a0 --- /dev/null +++ b/src/locales/modules/ciaoBug.js @@ -0,0 +1,115 @@ +import { _ } from '../helper.js' + +/** + * @homebridge/ciao Windows cmd 弹窗 bug 的用户提示文案 + * 上游 issue: https://github.com/homebridge/ciao/issues/64 + * 上游 PR: https://github.com/homebridge/ciao/pull/65 + */ +export default { + toastTitle: _( + '检测到已知问题:OpenClaw 运行时 Windows 上每 15 秒会弹一次 cmd 窗口', + 'Known issue detected: OpenClaw causes a cmd popup every 15s on Windows', + '偵測到已知問題:OpenClaw 執行時 Windows 每 15 秒會彈出 cmd 視窗', + '既知の問題を検出:Windows で OpenClaw 実行時、15 秒ごとに cmd ウィンドウが点滅します', + '알려진 문제 감지: Windows에서 OpenClaw 실행 시 15초마다 cmd 창이 깜박임', + ), + viewDetail: _( + '查看详情', + 'View details', + '檢視詳情', + '詳細を表示', + '자세히 보기', + ), + modalTitle: _( + 'Windows cmd 弹窗问题 — 第三方库 bug', + 'Windows cmd popup — third-party library bug', + 'Windows cmd 彈窗問題 — 第三方函式庫 bug', + 'Windows cmd ポップアップ — サードパーティ製ライブラリの不具合', + 'Windows cmd 팝업 — 서드파티 라이브러리 버그', + ), + summary: _( + '这是 OpenClaw 依赖的 @homebridge/ciao 库的已知 bug,不是 ClawPanel 或 OpenClaw 本身的问题。每 15-30 秒 ciao 会调用 arp -a 刷新网络接口缓存,但未使用 windowsHide 参数,所以 Windows 上会弹出一个短暂的 cmd 窗口。功能本身完全正常,只是视觉干扰。', + 'This is a known bug in @homebridge/ciao, which OpenClaw depends on. It is not a bug of ClawPanel or OpenClaw itself. Every 15–30 seconds ciao calls "arp -a" to refresh the network interface cache, but without the windowsHide option, so a cmd window flashes briefly on Windows. Functionality is unaffected — it is purely a visual annoyance.', + '這是 OpenClaw 相依的 @homebridge/ciao 函式庫的已知 bug,不是 ClawPanel 或 OpenClaw 本身的問題。每 15–30 秒 ciao 會呼叫 arp -a 重新整理網路介面快取,但沒有使用 windowsHide 參數,所以 Windows 上會彈出短暫的 cmd 視窗。功能本身完全正常,只是視覺干擾。', + 'これは OpenClaw が依存している @homebridge/ciao ライブラリの既知の不具合であり、ClawPanel や OpenClaw 本体の問題ではありません。ciao は 15〜30 秒ごとに "arp -a" を呼び出してネットワークインターフェースのキャッシュを更新しますが、windowsHide オプションが指定されていないため、Windows では cmd ウィンドウが一瞬点滅します。動作自体は正常で、視覚的な煩わしさのみです。', + '이것은 OpenClaw가 의존하는 @homebridge/ciao 라이브러리의 알려진 버그이며 ClawPanel이나 OpenClaw 자체의 문제가 아닙니다. ciao는 15~30초마다 "arp -a"를 호출하여 네트워크 인터페이스 캐시를 갱신하는데 windowsHide 옵션을 지정하지 않아 Windows에서 cmd 창이 순간적으로 깜박입니다. 기능 자체는 정상이며 시각적 방해일 뿐입니다.', + ), + envTitle: _( + '当前环境', + 'Environment', + '目前環境', + '現在の環境', + '현재 환경', + ), + pathLabel: _( + '源文件路径', + 'Source file', + '原始檔路徑', + 'ソースファイルパス', + '소스 파일 경로', + ), + fixTitle: _( + '解决方案', + 'How to fix', + '解決方式', + '対処方法', + '해결 방법', + ), + // HTML 允许:可包含超链接。escapeHtml 在这些条目上不启用。 + fixUpstream: _( + '等待上游合并 —— 上游已有 PR #65 提供修复,未合并。OpenClaw 升级 ciao 后自动消失。', + 'Wait for upstream mergePR #65 already provides the fix but has not been merged. Will disappear once OpenClaw upgrades its ciao dependency.', + '等待上游合併 —— 上游已有 PR #65 提供修復,尚未合併。OpenClaw 升級 ciao 後會自動消失。', + '上流のマージを待つ —— PR #65 で既に修正が提供されていますが、未マージです。OpenClaw が ciao を更新すれば自動的に解消されます。', + '업스트림 병합 대기 —— PR #65에 이미 수정이 올라와 있지만 병합되지 않았습니다. OpenClaw가 ciao 의존성을 업데이트하면 자동으로 사라집니다.', + ), + fixPatchPackage: _( + '使用 patch-package 给 OpenClaw 打补丁:在 OpenClaw 源码仓库(或 npm 全局安装目录下的 openclaw 包目录)执行 npx patch-package @homebridge/ciao,在 NetworkManager.js 的 exec 调用中加 { windowsHide: true }。', + 'Apply a patch-package patch to OpenClaw: in the OpenClaw source repo (or the globally installed openclaw directory), run npx patch-package @homebridge/ciao after adding { windowsHide: true } to the exec calls in NetworkManager.js.', + '使用 patch-package 為 OpenClaw 套用修補:在 OpenClaw 原始碼倉庫(或 npm 全域安裝目錄的 openclaw 包目錄)執行 npx patch-package @homebridge/ciao,在 NetworkManager.js 的 exec 呼叫中加入 { windowsHide: true }。', + 'patch-package で OpenClaw にパッチを適用:OpenClaw のソースリポジトリ(または npm グローバル インストール ディレクトリ内の openclaw パッケージ ディレクトリ)で npx patch-package @homebridge/ciao を実行し、NetworkManager.js の exec 呼び出しに { windowsHide: true } を追加してください。', + 'patch-package로 OpenClaw에 패치 적용: OpenClaw 소스 저장소(또는 npm 전역 설치 디렉터리의 openclaw 패키지 디렉터리)에서 NetworkManager.js의 exec 호출에 { windowsHide: true }를 추가한 뒤 npx patch-package @homebridge/ciao를 실행하세요.', + ), + fixManual: _( + '手动编辑 NetworkManager.js(最简单,但升级 openclaw 后需重做):用编辑器打开上面显示的文件路径,找到 6 处 child_process.exec("arp ...") 调用,在 URL 参数和回调之间加 { windowsHide: true },,保存后重启 Gateway。', + 'Manually edit NetworkManager.js (simplest, but you must redo it after upgrading openclaw): open the file at the path above, find the 6 child_process.exec("arp ...") calls, add { windowsHide: true }, between the first argument and the callback, save and restart Gateway.', + '手動編輯 NetworkManager.js(最簡單,但升級 openclaw 後需重做):用編輯器打開上面顯示的檔案路徑,找到 6 處 child_process.exec("arp ...") 呼叫,在 URL 參數和回呼之間加入 { windowsHide: true },,儲存後重新啟動 Gateway。', + 'NetworkManager.js を手動で編集(最も簡単ですが、openclaw を更新するたびにやり直しが必要):上記のパスのファイルを開き、child_process.exec("arp ...") の 6 箇所の呼び出しを探して、URL 引数とコールバックの間に { windowsHide: true }, を追加し、保存して Gateway を再起動してください。', + 'NetworkManager.js 수동 편집(가장 간단하지만 openclaw 업그레이드 후 다시 해야 함): 위 경로의 파일을 편집기로 열고 6개의 child_process.exec("arp ...") 호출을 찾아 URL 인수와 콜백 사이에 { windowsHide: true },를 추가한 후 저장하고 Gateway를 재시작하세요.', + ), + linkIssue: _( + '上游 Issue #64', + 'Upstream issue #64', + '上游 Issue #64', + '上流 Issue #64', + '업스트림 Issue #64', + ), + linkPr: _( + '上游修复 PR #65', + 'Upstream fix PR #65', + '上游修復 PR #65', + '上流修正 PR #65', + '업스트림 수정 PR #65', + ), + disclaimer: _( + '说明:ClawPanel 选择「检测并告知」而不是「自动修改你的 node_modules」—— 我们尊重你对本机软件的控制权。', + 'Note: ClawPanel chose "detect & inform" instead of "silently patch your node_modules" — we respect your control over local software.', + '說明:ClawPanel 選擇「偵測並告知」而不是「自動修改你的 node_modules」—— 我們尊重你對本機軟體的控制權。', + '注:ClawPanel は「検出して通知する」ことを選択しました。「node_modules を自動改変する」のではなく、ローカルソフトウェアに対するユーザーのコントロールを尊重しています。', + '참고: ClawPanel은 node_modules를 자동으로 수정하는 대신 "감지하고 알리는" 방식을 선택했습니다 — 로컬 소프트웨어에 대한 사용자의 통제권을 존중합니다.', + ), + dismissForVersion: _( + '已了解,不再提醒本版本', + 'Got it, don’t remind for this version', + '已了解,不再提醒本版本', + '了解しました。このバージョンでは再通知しません', + '이해했습니다, 이 버전에서는 다시 알리지 마세요', + ), + dismissed: _( + '已忽略此版本的提醒', + 'Reminder dismissed for this version', + '已忽略此版本的提醒', + 'このバージョンの通知を無視しました', + '이 버전의 알림을 무시했습니다', + ), +} diff --git a/src/main.js b/src/main.js index 2672b18..7d4a825 100644 --- a/src/main.js +++ b/src/main.js @@ -344,6 +344,17 @@ async function boot() { setTimeout(() => splash.remove(), 500) } + // 启动 3 秒后提示 @homebridge/ciao cmd 弹窗问题(仅 Windows 受影响) + // 只在桌面端跑——Web 模式下的 dev-api.js 桩会直接返回 affected:false + setTimeout(async () => { + try { + const { checkAndWarnCiaoBug } = await import('./lib/ciao-bug-warning.js') + checkAndWarnCiaoBug() + } catch (err) { + console.debug('[ciao-bug] module skipped:', err) + } + }, 3000) + // 默认密码提醒横幅 if (sessionStorage.getItem('clawpanel_must_change_pw') === '1') { const banner = document.createElement('div')