优化: run_command 实时流式 + 审批浮窗修复 + 文档探索闭环(fetch_url+obscura引导+model_fetch兼容+prompt策略+厂商预设)
This commit is contained in:
@@ -117,6 +117,11 @@ function displayName(tc: AiToolCallInfo): string {
|
||||
|
||||
let _unlistenUpdate: UnlistenFn | null = null
|
||||
let _positioned = false
|
||||
/** 关窗兜底:防止 close() promise 卡住或被拒导致「正在关闭…」永久停留。
|
||||
* close() 在 Tauri 2.x 触发 closeRequested(非强制),某些情况下原生窗口未及时销毁 →
|
||||
* Vue 组件保持挂载 + 空态 hint 永久可见。此处超时强制 destroy() 兜底。 */
|
||||
let _closeFallbackTimer: ReturnType<typeof setTimeout> | null = null
|
||||
const CLOSE_TIMEOUT_MS = 1500
|
||||
|
||||
onMounted(async () => {
|
||||
// 首屏定位到屏幕右上角(只定位一次,后续保留用户拖动后位置)
|
||||
@@ -164,8 +169,51 @@ onMounted(async () => {
|
||||
onBeforeUnmount(() => {
|
||||
_unlistenUpdate?.()
|
||||
_unlistenUpdate = null
|
||||
if (_closeFallbackTimer) {
|
||||
clearTimeout(_closeFallbackTimer)
|
||||
_closeFallbackTimer = null
|
||||
}
|
||||
})
|
||||
|
||||
/** 关窗统一实现:close 先行(优雅),超时/失败兜底 destroy()(强制销毁)。
|
||||
* 根因修复(BUG3+):原实现仅 await getCurrentWebviewWindow().close(),而 Tauri 2.x 的
|
||||
* close() 触发 closeRequested(非强制),若原生窗口因任意原因未及时销毁(已知偶发),
|
||||
* Vue 组件保持挂载 → 浮窗内空态 hint「正在关闭…」永久可见,UI 卡死。
|
||||
* - 兜底1:close() 失败 → 立即 destroy() 强制销毁
|
||||
* - 兜底2:close() 1.5s 未完成 → destroy() 强制销毁(超时定时器)
|
||||
* - 兜底3:destroy() 也失败 → 控制台报错(不再静默吞掉,便于排查) */
|
||||
async function closeWithFallback(): Promise<void> {
|
||||
const win = getCurrentWebviewWindow()
|
||||
// 超时兜底:close 卡住则强制 destroy
|
||||
_closeFallbackTimer = setTimeout(() => {
|
||||
_closeFallbackTimer = null
|
||||
console.warn('[ApprovalPopup] close() 超时,降级 destroy()')
|
||||
win.destroy().catch((e: unknown) => {
|
||||
console.error('[ApprovalPopup] destroy() 兜底也失败:', e)
|
||||
})
|
||||
}, CLOSE_TIMEOUT_MS)
|
||||
try {
|
||||
await win.close()
|
||||
// close 成功:取消超时定时器(若已触发则 destroy 已并行无害)
|
||||
if (_closeFallbackTimer) {
|
||||
clearTimeout(_closeFallbackTimer)
|
||||
_closeFallbackTimer = null
|
||||
}
|
||||
} catch (e) {
|
||||
// close 失败:取消超时,降级 destroy()
|
||||
if (_closeFallbackTimer) {
|
||||
clearTimeout(_closeFallbackTimer)
|
||||
_closeFallbackTimer = null
|
||||
}
|
||||
console.warn('[ApprovalPopup] close() 失败,降级 destroy():', e)
|
||||
try {
|
||||
await win.destroy()
|
||||
} catch (e2) {
|
||||
console.error('[ApprovalPopup] destroy() 兜底也失败:', e2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 批准:risk 类 approve(true),path 类 authorizeDir('once') */
|
||||
async function onApprove(tc: AiToolCallInfo) {
|
||||
if (processingId.value === tc.id) return
|
||||
@@ -212,12 +260,15 @@ async function onReject(tc: AiToolCallInfo) {
|
||||
* 误以为卡死。空 = 完成,应即时关浮窗(不等待主窗口 watch closePopup 的跨窗口往返)。 */
|
||||
async function maybeAutoClose() {
|
||||
if (approvals.value.length > 0) return
|
||||
// 先关窗(确保用户立刻看到反馈),再 emit 清主窗口引用(顺序与 onClose 一致)
|
||||
// 关窗走 closeWithFallback 兜底(close 超时/失败 → destroy 强制销毁),防「正在关闭…」永久
|
||||
await closeWithFallback()
|
||||
try {
|
||||
const { emit } = await import('@tauri-apps/api/event')
|
||||
// 先关窗(确保用户立刻看到反馈),再 emit 清主窗口引用(顺序与 onClose 一致)
|
||||
await getCurrentWebviewWindow().close()
|
||||
await emit(ApprovalPopupEvents.EVT_CLOSED, {})
|
||||
} catch { /* ignore */ }
|
||||
} catch (e) {
|
||||
console.warn('[ApprovalPopup] emit EVT_CLOSED 失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
/** 点击「打开审批面板」:emit 主窗口聚焦 + togglePanel */
|
||||
@@ -233,13 +284,14 @@ async function onActivate() {
|
||||
* 改为先 close(即时反馈,core:window:allow-close 已授权),再 emit 清主窗口引用;
|
||||
* 即便 emit 失败,主窗口 win.once('tauri://destroyed') 也会清 _popupWin(双保险)。 */
|
||||
async function onClose() {
|
||||
try {
|
||||
await getCurrentWebviewWindow().close()
|
||||
} catch { /* ignore */ }
|
||||
// 关窗走 closeWithFallback 兜底(close 超时/失败 → destroy 强制销毁)
|
||||
await closeWithFallback()
|
||||
try {
|
||||
const { emit } = await import('@tauri-apps/api/event')
|
||||
await emit(ApprovalPopupEvents.EVT_CLOSED, {})
|
||||
} catch { /* ignore */ }
|
||||
} catch (e) {
|
||||
console.warn('[ApprovalPopup] emit EVT_CLOSED 失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 拖动样式反馈(实际拖动由 data-tauri-drag-region + Tauri dataDrag 守卫处理)
|
||||
|
||||
@@ -533,4 +533,6 @@ async function runImport() {
|
||||
}
|
||||
.toast-info { background: var(--df-accent); color: #fff; }
|
||||
.toast-error { background: var(--df-danger); color: #fff; }
|
||||
/* P0-2: ToastType 新增 'success' 兜底样式(本视图当前未用,补全避免裸类) */
|
||||
.toast-success { background: var(--df-success); color: #fff; }
|
||||
</style>
|
||||
|
||||
@@ -221,6 +221,8 @@ onUnmounted(() => {
|
||||
.toast-error { background: var(--df-danger-bg); color: var(--df-danger); border: 0.5px solid var(--df-danger); }
|
||||
.toast-warning { background: var(--df-warning-bg); color: var(--df-warning); border: 0.5px solid var(--df-warning); }
|
||||
.toast-info { background: var(--df-accent-bg); color: var(--df-accent); border: 0.5px solid var(--df-accent); }
|
||||
/* P0-2: 保存成功明确反馈(绿色),与 info(中性强调)区分 */
|
||||
.toast-success { background: var(--df-success-bg); color: var(--df-success); border: 0.5px solid var(--df-success); }
|
||||
.toast-enter-active, .toast-leave-active { transition: opacity 0.2s, transform 0.2s; }
|
||||
.toast-enter-from, .toast-leave-to { opacity: 0; transform: translate(-50%, -8px); }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user