优化: 审批超时倒计时(60s变红)+分离窗口监听核验
This commit is contained in:
@@ -138,8 +138,15 @@ function getApprovalTimeoutMs(): number {
|
||||
return raw
|
||||
}
|
||||
|
||||
/** toolCallId → 审批超时计时器 */
|
||||
const _approvalTimers = new Map<string, ReturnType<typeof setTimeout>>()
|
||||
/** toolCallId → 审批超时计时器 + 起点信息(startedAt/timeoutMs 供前端到期倒计时读剩余) */
|
||||
interface ApprovalTimerInfo {
|
||||
timer: ReturnType<typeof setTimeout>
|
||||
/** 计时器启动时刻(ms)——即本笔审批超时窗口的真实起点 */
|
||||
startedAt: number
|
||||
/** 本计时器生效的超时阈值(ms) */
|
||||
timeoutMs: number
|
||||
}
|
||||
const _approvalTimers = new Map<string, ApprovalTimerInfo>()
|
||||
|
||||
/**
|
||||
* 启动审批超时计时器(幂等:同 id 重复启动不重建)。
|
||||
@@ -176,21 +183,33 @@ export function startApprovalTimer(
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
}, timeoutMs)
|
||||
_approvalTimers.set(toolCallId, timer)
|
||||
_approvalTimers.set(toolCallId, { timer, startedAt: Date.now(), timeoutMs })
|
||||
}
|
||||
|
||||
/**
|
||||
* 取某审批距前端自动拒绝的剩余毫秒。
|
||||
* 审批超时是前端本计时器驱动的(到点才调 IPC 拒绝),故剩余值与实际拒绝时刻精确对齐,
|
||||
* 优于后端 created_at(不序列化透传)或 ToolCard 本地到达时间的近似。
|
||||
* 返回 null = 无活跃计时器(未挂起/已到期/用户配置不限时),调用方应回退其他展示。
|
||||
*/
|
||||
export function getApprovalRemainingMs(toolCallId: string): number | null {
|
||||
const info = _approvalTimers.get(toolCallId)
|
||||
if (!info) return null
|
||||
return info.timeoutMs - (Date.now() - info.startedAt)
|
||||
}
|
||||
|
||||
/** 清除单个审批计时器 */
|
||||
export function clearApprovalTimer(toolCallId: string): void {
|
||||
const timer = _approvalTimers.get(toolCallId)
|
||||
if (timer) {
|
||||
clearTimeout(timer)
|
||||
const info = _approvalTimers.get(toolCallId)
|
||||
if (info) {
|
||||
clearTimeout(info.timer)
|
||||
_approvalTimers.delete(toolCallId)
|
||||
}
|
||||
}
|
||||
|
||||
/** 清除全部审批计时器(整流超时收尾/组件卸载时调用) */
|
||||
export function clearAllApprovalTimers(): void {
|
||||
for (const timer of _approvalTimers.values()) clearTimeout(timer)
|
||||
for (const info of _approvalTimers.values()) clearTimeout(info.timer)
|
||||
_approvalTimers.clear()
|
||||
}
|
||||
|
||||
|
||||
@@ -105,6 +105,14 @@ async function detachPanel(convId?: string) {
|
||||
// 异步传播。消除 detached=true 到 AiChat 实际卸载的窗口期(reattach 重叠期/HMR/极速 mount-unmount
|
||||
// 竞态下,onBeforeUnmount 可能延迟,旧 listener 残留会与分离窗口新 listener 并存竞态)。
|
||||
// stopListener 同步释放后端 listener + 清看门狗,分离窗口 resumeInDetached 起自己的 startListener。
|
||||
//
|
||||
// BUG-260624-02 核验结论(2026-08-15):分离窗口事件监听独立存在,审批事件无缺口。
|
||||
// 链路:本窗口 url '#/ai-detached' → AiDetached.vue 挂 AiChat(detached) → AiChat onMounted
|
||||
// 调 store.startListener()(独立 JS context 各自注册);后端 ai-chat-event 为 app 级广播
|
||||
// (emit 到全部 webview),故审批类事件(工具 pending/审批结果)分离窗口自身可收。
|
||||
// 前端派生的 'ai-pending-arrived'(useAiStreamingEvents/useAiToolEvents emit)亦为全局广播,
|
||||
// 分离窗口 ToolCardList 的 listener 同样收到。主窗口 listener 在此 stopListener 释清,
|
||||
// 不与分离窗口并存竞态。审批超时计时器(aiShared)同样随 listener 在接管窗口内重建。
|
||||
stopListener()
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import { ref, reactive, computed, watch, onBeforeUnmount, type Ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useConfirm } from '@/composables/useConfirm'
|
||||
import { aiApi } from '@/api'
|
||||
import { getApprovalRemainingMs } from './aiShared'
|
||||
import type { AiToolCallInfo } from '@/api/types'
|
||||
|
||||
/**
|
||||
@@ -103,6 +104,20 @@ export function useToolApproval(tc: Ref<AiToolCallInfo>, emit: ToolApprovalEmit)
|
||||
return 'normal'
|
||||
})
|
||||
|
||||
// ── 审批到期倒计时(BUG-260624-02)──
|
||||
// 剩余 = aiShared 审批超时计时器真实起点推算(自动拒绝时刻精确对齐,非本地近似)。
|
||||
// 读 waitSecs 建立每秒响应依赖(计时 tick 触发重算);null = 无活跃计时器
|
||||
// (不限时/未挂起/已到期),调用方回退挂起时长展示。
|
||||
const expireSecs = computed<number | null>(() => {
|
||||
void waitSecs.value
|
||||
const remain = getApprovalRemainingMs(tc.value.id)
|
||||
if (remain === null || remain <= 0) return null
|
||||
return Math.ceil(remain / 1000)
|
||||
})
|
||||
// 到期前 60s 变红(复用 waitLevel 的 urgent 样式:红色脉冲)
|
||||
const expireLevel = computed<'normal' | 'urgent'>(() =>
|
||||
expireSecs.value !== null && expireSecs.value <= 60 ? 'urgent' : 'normal')
|
||||
|
||||
// ── 审批超时兜底 toast ──
|
||||
const approveToast = reactive({ visible: false, msg: '' })
|
||||
let _approveToastTimer: ReturnType<typeof setTimeout> | null = null
|
||||
@@ -274,6 +289,8 @@ export function useToolApproval(tc: Ref<AiToolCallInfo>, emit: ToolApprovalEmit)
|
||||
waitSecs,
|
||||
waitLevel,
|
||||
formatWaitTime,
|
||||
expireSecs,
|
||||
expireLevel,
|
||||
onApprove,
|
||||
onReject,
|
||||
onAuthorizeOnce,
|
||||
|
||||
Reference in New Issue
Block a user