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
This commit is contained in:
晴天
2026-04-24 19:36:20 +08:00
committed by GitHub
parent da5adc5843
commit 11cd6218dc
8 changed files with 439 additions and 1 deletions

136
src/lib/ciao-bug-warning.js Normal file
View File

@@ -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
? `<div class="ciao-bug-row"><span class="muted">@homebridge/ciao</span> <code>${escapeHtml(result.version)}</code></div>`
: ''
const pathLine = result.networkManagerPath
? `<div class="ciao-bug-row"><span class="muted">${escapeHtml(t('ciaoBug.pathLabel'))}</span> <code>${escapeHtml(result.networkManagerPath)}</code></div>`
: ''
overlay.innerHTML = `
<div class="modal" style="max-width:640px;">
<div class="modal-title">${escapeHtml(t('ciaoBug.modalTitle'))}</div>
<div class="modal-body" style="font-size:var(--font-size-sm);line-height:1.6;">
<p style="margin:0 0 12px;">${escapeHtml(t('ciaoBug.summary'))}</p>
<h4 style="margin:16px 0 6px;font-size:13px;color:var(--text-secondary);">${escapeHtml(t('ciaoBug.envTitle'))}</h4>
<div class="ciao-bug-env" style="font-size:12px;color:var(--text-secondary);word-break:break-all;">
${versionLine}
${pathLine}
</div>
<h4 style="margin:16px 0 6px;font-size:13px;color:var(--text-secondary);">${escapeHtml(t('ciaoBug.fixTitle'))}</h4>
<ol style="margin:0;padding-left:20px;">
<li style="margin-bottom:6px;">${t('ciaoBug.fixUpstream')}</li>
<li style="margin-bottom:6px;">${t('ciaoBug.fixPatchPackage')}</li>
<li style="margin-bottom:6px;">${t('ciaoBug.fixManual')}</li>
</ol>
<div style="margin-top:14px;display:flex;gap:12px;flex-wrap:wrap;font-size:12px;">
<a href="https://github.com/homebridge/ciao/issues/64" target="_blank" rel="noopener" style="color:var(--accent);">${escapeHtml(t('ciaoBug.linkIssue'))}</a>
<a href="https://github.com/homebridge/ciao/pull/65" target="_blank" rel="noopener" style="color:var(--accent);">${escapeHtml(t('ciaoBug.linkPr'))}</a>
</div>
<p style="margin:16px 0 0;font-size:12px;color:var(--text-tertiary);">${escapeHtml(t('ciaoBug.disclaimer'))}</p>
</div>
<div class="modal-actions">
<button class="btn btn-secondary btn-sm" data-action="close">${escapeHtml(t('common.close'))}</button>
<button class="btn btn-primary btn-sm" data-action="dismiss">${escapeHtml(t('ciaoBug.dismissForVersion'))}</button>
</div>
</div>
`
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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
}

View File

@@ -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),