优化: 审批超时倒计时(60s变红)+分离窗口监听核验
This commit is contained in:
+1
-1
@@ -309,7 +309,7 @@ graph TD
|
||||
### ✅ 2026-06-24 aichat 可靠性修复(消息重叠×5 + 授权弹窗卡死 + 工具执行心跳误报 · 诊断workflow→论证workflow)
|
||||
|
||||
> 剩余低优 [ ](非阻断):
|
||||
> - [ ] BUG-260624-02 残留:审批超时倒计时 UI(5min 无倒计时,ToolCard pending 态显 mm:ss 到期前 60s 变红)+ 核 detached 窗口 emit 作用域(分离窗口是否独立 startListener,否则 ai-pending-arrived 收不到)
|
||||
> - [x] BUG-260624-02 残留 ✅ 已销账(2026-08-15):ToolCard pending 态倒计时落地(`_approvalTimers` 升级携带 startedAt/timeoutMs + `getApprovalRemainingMs` 真值起点,expireSecs/expireLevel computed,≤60s `.is-urgent` 红;不限时回退正计时;i18n expireIn zh/en)+ detached 窗口核验无缺口(AiDetached→AiChat 独立 startListener,ai-chat-event app 级广播,主窗口 detach 时 stopListener 不竞态,结论注释 useAiWindow:107-115)
|
||||
> - [x] F-09 根因:streaming/currentText 全局单例改 per-conv ✅ **2026-08-07 核验销账**(per_conv HashMap + 前端 convStates Map enum 轨 + AiConvStateChanged 事件 per-conv 路由,根治多会话串扰)(根治多会话串扰,见 docs/待决策.md)
|
||||
> - [ ] CR-260622-01-P2-2 [P2低优·评估降级] — **HTML/markdown/JSON 等非代码文件无 session 级缓存**。**2026-06-24 评估**:已有 `TOOL_RESULT_COMPRESS_ENABLED`(mod.rs:98/1029-1078 view-only 摘要,>2KB tool_result 压缩,LLM 视图摘要非全文回灌 prompt)部分缓解;read_symbol 治代码文件(主场景,降 24.4x)。非代码文件完整 session 缓存(path→hash+content+patch失效)设计复杂 + LLM patch 后重读确认行为不确定(缓存命中提示可能不够 LLM 仍重读),归 B 路线 prompt 策略(约束 patch 后不重读)更合适,工具侧完整缓存低优暂缓。
|
||||
|
||||
|
||||
@@ -40,8 +40,12 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="tc.reason" class="ai-tool-approval-reason">⚠ {{ tc.reason }}</div>
|
||||
<!-- BUG-260624-02: 审批挂起时长提示(>2分钟橙色,>5分钟红色) -->
|
||||
<div v-if="waitSecs > 0" class="ai-tool-wait-time" :class="'is-' + waitLevel">
|
||||
<!-- BUG-260624-02: 审批到期倒计时(与前端自动拒绝时刻精确对齐,≤60s 红色脉冲);
|
||||
无活跃计时器(不限时/已到期)回退显挂起时长(>2分钟橙色,>5分钟红色) -->
|
||||
<div v-if="expireSecs !== null" class="ai-tool-wait-time" :class="'is-' + expireLevel">
|
||||
{{ $t('aiTool.expireIn', { time: formatWaitTime(expireSecs) }) }}
|
||||
</div>
|
||||
<div v-else-if="waitSecs > 0" class="ai-tool-wait-time" :class="'is-' + waitLevel">
|
||||
⏱ {{ $t('aiTool.waitTime', { time: formatWaitTime(waitSecs) }) }}
|
||||
</div>
|
||||
<!-- AE-2025-03: write_file 审批 diff 预览(红删绿增,行级)。旧文件不存在→tc.diff 为空,回退显上方 args content -->
|
||||
@@ -188,6 +192,8 @@ const {
|
||||
waitSecs,
|
||||
waitLevel,
|
||||
formatWaitTime,
|
||||
expireSecs,
|
||||
expireLevel,
|
||||
onApprove,
|
||||
onReject,
|
||||
onAuthorizeOnce,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -10,6 +10,8 @@ export default {
|
||||
executionSlow: 'Tool "{name}" is taking longer than expected, please wait…',
|
||||
// approval pending duration hint (>2min orange, >5min red)
|
||||
waitTime: '⏱ Waiting {time}',
|
||||
// approval expiry countdown (BUG-260624-02, aligned with frontend auto-reject, red ≤60s)
|
||||
expireIn: '⏳ Auto-reject in {time}',
|
||||
// tool execution failure marker (AR-6 keeps status=completed, red to distinguish from success)
|
||||
executionFailed: 'Failed',
|
||||
executionFailedHint: 'Tool execution failed, see details below',
|
||||
|
||||
@@ -10,6 +10,8 @@ export default {
|
||||
executionSlow: '工具「{name}」执行较久,请稍候…',
|
||||
// 审批挂起时长提示(>2分钟橙色,>5分钟红色)
|
||||
waitTime: '⏱ 已等待 {time}',
|
||||
// 审批到期倒计时(BUG-260624-02,与前端自动拒绝时刻对齐,≤60s 红色)
|
||||
expireIn: '⏳ {time} 后超时自动拒绝',
|
||||
// 工具执行失败视觉标识(AR-6 下 status=completed,需红色区分于成功)
|
||||
executionFailed: '执行失败',
|
||||
executionFailedHint: '工具执行失败,详情见下方',
|
||||
|
||||
Reference in New Issue
Block a user