修复: 审批浮窗无法关闭(托盘拦截误伤+兜底失效)
根因=关窗拦截全局生效误伤浮窗等JS窗口,close被吞成隐藏僵尸永不销毁, 且close() resolve≠销毁使前端兜底被误取消;改仅主窗口拦截,关窗兜底 按tauri://destroyed判定超时强制销毁,主窗口closePopup同步加固
This commit is contained in:
@@ -510,9 +510,14 @@ pub fn run() {
|
|||||||
// Phase3: 内嵌 HTTP MCP server 状态查询
|
// Phase3: 内嵌 HTTP MCP server 状态查询
|
||||||
crate::mcp::mcp_get_status,
|
crate::mcp::mcp_get_status,
|
||||||
])
|
])
|
||||||
// Phase3: 关窗口不退出,隐藏到托盘常驻(托盘「退出」菜单才真正结束进程)
|
// 关主窗口不退出,隐藏到托盘常驻(托盘「退出」菜单才真正结束进程)。
|
||||||
|
// 仅拦 main:approval-popup / ai-detached / fe-detached 等 JS 创建窗口必须放行
|
||||||
|
// 真正关闭,否则 close 被吞成隐藏僵尸窗,浮窗「正在关闭…」卡死、新审批推给不可见窗口。
|
||||||
.on_window_event(|window, event| {
|
.on_window_event(|window, event| {
|
||||||
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
|
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
|
||||||
|
if window.label() != "main" {
|
||||||
|
return; // 非主窗口放行真正销毁
|
||||||
|
}
|
||||||
api.prevent_close(); // 拦截真正关闭
|
api.prevent_close(); // 拦截真正关闭
|
||||||
let _ = window.hide(); // 隐藏到托盘
|
let _ = window.hide(); // 隐藏到托盘
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ const EVT_ACTIVATE = 'approval-popup-activate'
|
|||||||
const EVT_CLOSED = 'approval-popup-closed'
|
const EVT_CLOSED = 'approval-popup-closed'
|
||||||
|
|
||||||
/** 模块级:当前浮窗引用 + 是否用户手动关闭(防 watch 重复弹) */
|
/** 模块级:当前浮窗引用 + 是否用户手动关闭(防 watch 重复弹) */
|
||||||
type PopupWin = { close: () => Promise<void> }
|
type PopupWin = { close: () => Promise<void>; destroy: () => Promise<void> }
|
||||||
let _popupWin: PopupWin | null = null
|
let _popupWin: PopupWin | null = null
|
||||||
let _userClosed = false
|
let _userClosed = false
|
||||||
let _unlistenActivate: (() => void) | null = null
|
let _unlistenActivate: (() => void) | null = null
|
||||||
@@ -138,14 +138,35 @@ async function openOrUpdate(approvals: AiToolCallInfo[]): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 关闭浮窗(若存在) */
|
/** close() 超时降级 destroy() 的阈值(对齐浮窗侧 closeWithFallback 的 CLOSE_TIMEOUT_MS) */
|
||||||
|
const CLOSE_TIMEOUT_MS = 1500
|
||||||
|
|
||||||
|
/** 关闭浮窗(若存在)。
|
||||||
|
* 根因:Tauri 2.x 的 close() 走 closeRequested(非强制),偶发原生窗口未及时销毁,而主窗口侧
|
||||||
|
* 此前裸 await close() 无兜底 → 浮窗空态 hint「正在关闭…」永久残留。
|
||||||
|
* 对齐浮窗侧 closeWithFallback 语义:取引用后立即置空 _popupWin(防重复关/防关闭期间拿到旧引用),
|
||||||
|
* close 失败或超时未落定 → destroy() 强制销毁。 */
|
||||||
async function closePopup(): Promise<void> {
|
async function closePopup(): Promise<void> {
|
||||||
|
const win = _popupWin
|
||||||
|
_popupWin = null
|
||||||
|
if (!win) return
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
console.warn('[ApprovalPopup] close() 超时,降级 destroy()')
|
||||||
|
win.destroy().catch((e: unknown) => {
|
||||||
|
console.error('[ApprovalPopup] destroy() 兜底也失败:', e)
|
||||||
|
})
|
||||||
|
}, CLOSE_TIMEOUT_MS)
|
||||||
try {
|
try {
|
||||||
if (!_popupWin) return
|
await win.close()
|
||||||
await _popupWin.close()
|
clearTimeout(timer)
|
||||||
_popupWin = null
|
} catch (e) {
|
||||||
} catch {
|
clearTimeout(timer)
|
||||||
/* 浮窗已被关/不可用,忽略 */
|
console.warn('[ApprovalPopup] close() 失败,降级 destroy():', e)
|
||||||
|
try {
|
||||||
|
await win.destroy()
|
||||||
|
} catch (e2) {
|
||||||
|
console.error('[ApprovalPopup] destroy() 兜底也失败:', e2)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+27
-25
@@ -246,10 +246,7 @@ function _syncArrivalTimes(list: AiToolCallInfo[]) {
|
|||||||
|
|
||||||
let _unlistenUpdate: UnlistenFn | null = null
|
let _unlistenUpdate: UnlistenFn | null = null
|
||||||
let _positioned = false
|
let _positioned = false
|
||||||
/** 关窗兜底:防止 close() promise 卡住或被拒导致「正在关闭…」永久停留。
|
/** close() 后窗口仍未销毁则强制 destroy() 的阈值(closeWithFallback 使用) */
|
||||||
* close() 在 Tauri 2.x 触发 closeRequested(非强制),某些情况下原生窗口未及时销毁 →
|
|
||||||
* Vue 组件保持挂载 + 空态 hint 永久可见。此处超时强制 destroy() 兜底。 */
|
|
||||||
let _closeFallbackTimer: ReturnType<typeof setTimeout> | null = null
|
|
||||||
const CLOSE_TIMEOUT_MS = 1500
|
const CLOSE_TIMEOUT_MS = 1500
|
||||||
|
|
||||||
/** Esc 键关浮窗(键盘可达性)。模块级命名函数,onBeforeUnmount 据此精确移除同一引用。
|
/** Esc 键关浮窗(键盘可达性)。模块级命名函数,onBeforeUnmount 据此精确移除同一引用。
|
||||||
@@ -349,6 +346,13 @@ onMounted(async () => {
|
|||||||
_tickWaitSecs()
|
_tickWaitSecs()
|
||||||
if (_waitTimer) clearInterval(_waitTimer)
|
if (_waitTimer) clearInterval(_waitTimer)
|
||||||
_waitTimer = setInterval(_tickWaitSecs, 1000)
|
_waitTimer = setInterval(_tickWaitSecs, 1000)
|
||||||
|
|
||||||
|
// 弹出即空态自检:创建竞态下浮窗可能收到已过期的空快照(主窗口 openOrUpdate 推快照时
|
||||||
|
// 列表恰好已清空),此后不会再有任何 EVT_UPDATE → 无人触发关闭,空态「正在关闭…」永久残留。
|
||||||
|
// mount 2s 后仍为空则主动走 maybeAutoClose 兜底关窗(正常有数据时此自检无操作)。
|
||||||
|
setTimeout(() => {
|
||||||
|
if (approvals.value.length === 0) void maybeAutoClose()
|
||||||
|
}, 2000)
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
@@ -358,42 +362,40 @@ onBeforeUnmount(() => {
|
|||||||
window.removeEventListener('keydown', _onKeyDown)
|
window.removeEventListener('keydown', _onKeyDown)
|
||||||
// H1:清挂起计时器(防卸载后仍 tick 致空 Map 操作/内存泄漏)
|
// H1:清挂起计时器(防卸载后仍 tick 致空 Map 操作/内存泄漏)
|
||||||
if (_waitTimer) { clearInterval(_waitTimer); _waitTimer = null }
|
if (_waitTimer) { clearInterval(_waitTimer); _waitTimer = null }
|
||||||
if (_closeFallbackTimer) {
|
|
||||||
clearTimeout(_closeFallbackTimer)
|
|
||||||
_closeFallbackTimer = null
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
/** 关窗统一实现:close 先行(优雅),超时/失败兜底 destroy()(强制销毁)。
|
/** 关窗统一实现:close 先行(优雅),超时/失败兜底 destroy()(强制销毁)。
|
||||||
* 根因修复(BUG3+):原实现仅 await getCurrentWebviewWindow().close(),而 Tauri 2.x 的
|
* 根因修复(BUG3+):原实现仅 await getCurrentWebviewWindow().close(),而 Tauri 2.x 的
|
||||||
* close() 触发 closeRequested(非强制),若原生窗口因任意原因未及时销毁(已知偶发),
|
* close() 触发 closeRequested(非强制),若原生窗口因任意原因未及时销毁(已知偶发),
|
||||||
* Vue 组件保持挂载 → 浮窗内空态 hint「正在关闭…」永久可见,UI 卡死。
|
* Vue 组件保持挂载 → 浮窗内空态 hint「正在关闭…」永久可见,UI 卡死。
|
||||||
* - 兜底1:close() 失败 → 立即 destroy() 强制销毁
|
* 判定依据:close() 只是派发关闭消息即 resolve,即使关闭被原生层 prevent_close 拦截
|
||||||
* - 兜底2:close() 1.5s 未完成 → destroy() 强制销毁(超时定时器)
|
* (窗口仅隐藏未销毁),promise 照样成功 → 不能以 resolve 作为「窗口已关」依据,
|
||||||
* - 兜底3:destroy() 也失败 → 控制台报错(不再静默吞掉,便于排查) */
|
* 以 once('tauri://destroyed') 置位 destroyed 标志为准,超时触发时若尚未销毁才 destroy。
|
||||||
|
* - 兜底1:close() 失败 → 未销毁则立即 destroy() 强制销毁
|
||||||
|
* - 兜底2:close() resolve 后 1.5s 仍未销毁 → destroy() 强制销毁(超时定时器)
|
||||||
|
* - 兜底3:destroy() 也失败 → 控制台报错(不再静默吞掉,便于排查)
|
||||||
|
* 兜底定时器不取消:并发调用(onClose 与 maybeAutoClose)各自持有 timer,
|
||||||
|
* destroy 幂等无害,以 destroyed 事件兜住重复强制销毁。 */
|
||||||
async function closeWithFallback(): Promise<void> {
|
async function closeWithFallback(): Promise<void> {
|
||||||
const win = getCurrentWebviewWindow()
|
const win = getCurrentWebviewWindow()
|
||||||
// 超时兜底:close 卡住则强制 destroy
|
// 以 destroyed 事件为「窗口真正销毁」的判定依据(close() resolve≠销毁,见上)
|
||||||
_closeFallbackTimer = setTimeout(() => {
|
let destroyed = false
|
||||||
_closeFallbackTimer = null
|
void win.once('tauri://destroyed', () => { destroyed = true })
|
||||||
console.warn('[ApprovalPopup] close() 超时,降级 destroy()')
|
// 超时兜底:close 后窗口迟迟未销毁则强制 destroy
|
||||||
|
setTimeout(() => {
|
||||||
|
if (destroyed) return
|
||||||
|
console.warn('[ApprovalPopup] close() 后窗口未销毁,降级 destroy()')
|
||||||
win.destroy().catch((e: unknown) => {
|
win.destroy().catch((e: unknown) => {
|
||||||
console.error('[ApprovalPopup] destroy() 兜底也失败:', e)
|
console.error('[ApprovalPopup] destroy() 兜底也失败:', e)
|
||||||
})
|
})
|
||||||
}, CLOSE_TIMEOUT_MS)
|
}, CLOSE_TIMEOUT_MS)
|
||||||
try {
|
try {
|
||||||
await win.close()
|
await win.close()
|
||||||
// close 成功:取消超时定时器(若已触发则 destroy 已并行无害)
|
// close() resolve 不取消兜底定时器:被 prevent_close 时 promise 照样成功而窗口未必销毁,
|
||||||
if (_closeFallbackTimer) {
|
// 是否强制 destroy 交给 destroyed 标志与超时判定(见上)
|
||||||
clearTimeout(_closeFallbackTimer)
|
|
||||||
_closeFallbackTimer = null
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// close 失败:取消超时,降级 destroy()
|
// close 失败:未销毁则降级 destroy()
|
||||||
if (_closeFallbackTimer) {
|
if (destroyed) return
|
||||||
clearTimeout(_closeFallbackTimer)
|
|
||||||
_closeFallbackTimer = null
|
|
||||||
}
|
|
||||||
console.warn('[ApprovalPopup] close() 失败,降级 destroy():', e)
|
console.warn('[ApprovalPopup] close() 失败,降级 destroy():', e)
|
||||||
try {
|
try {
|
||||||
await win.destroy()
|
await win.destroy()
|
||||||
|
|||||||
Reference in New Issue
Block a user