diff --git a/docs/todo.md b/docs/todo.md index 4e74768..1ef0bde 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -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 后不重读)更合适,工具侧完整缓存低优暂缓。 diff --git a/src/components/ToolCard.vue b/src/components/ToolCard.vue index 922dcbb..7ed3c8a 100644 --- a/src/components/ToolCard.vue +++ b/src/components/ToolCard.vue @@ -40,8 +40,12 @@
⚠ {{ tc.reason }}
- -
+ +
+ {{ $t('aiTool.expireIn', { time: formatWaitTime(expireSecs) }) }} +
+
⏱ {{ $t('aiTool.waitTime', { time: formatWaitTime(waitSecs) }) }}
@@ -188,6 +192,8 @@ const { waitSecs, waitLevel, formatWaitTime, + expireSecs, + expireLevel, onApprove, onReject, onAuthorizeOnce, diff --git a/src/composables/ai/aiShared.ts b/src/composables/ai/aiShared.ts index a6f06ca..9be5c7c 100644 --- a/src/composables/ai/aiShared.ts +++ b/src/composables/ai/aiShared.ts @@ -138,8 +138,15 @@ function getApprovalTimeoutMs(): number { return raw } -/** toolCallId → 审批超时计时器 */ -const _approvalTimers = new Map>() +/** toolCallId → 审批超时计时器 + 起点信息(startedAt/timeoutMs 供前端到期倒计时读剩余) */ +interface ApprovalTimerInfo { + timer: ReturnType + /** 计时器启动时刻(ms)——即本笔审批超时窗口的真实起点 */ + startedAt: number + /** 本计时器生效的超时阈值(ms) */ + timeoutMs: number +} +const _approvalTimers = new Map() /** * 启动审批超时计时器(幂等:同 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() } diff --git a/src/composables/ai/useAiWindow.ts b/src/composables/ai/useAiWindow.ts index 5cb59ec..f2f0a03 100644 --- a/src/composables/ai/useAiWindow.ts +++ b/src/composables/ai/useAiWindow.ts @@ -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() } diff --git a/src/composables/ai/useToolApproval.ts b/src/composables/ai/useToolApproval.ts index 2bd2a0d..a91ce45 100644 --- a/src/composables/ai/useToolApproval.ts +++ b/src/composables/ai/useToolApproval.ts @@ -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, emit: ToolApprovalEmit) return 'normal' }) + // ── 审批到期倒计时(BUG-260624-02)── + // 剩余 = aiShared 审批超时计时器真实起点推算(自动拒绝时刻精确对齐,非本地近似)。 + // 读 waitSecs 建立每秒响应依赖(计时 tick 触发重算);null = 无活跃计时器 + // (不限时/未挂起/已到期),调用方回退挂起时长展示。 + const expireSecs = computed(() => { + 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 | null = null @@ -274,6 +289,8 @@ export function useToolApproval(tc: Ref, emit: ToolApprovalEmit) waitSecs, waitLevel, formatWaitTime, + expireSecs, + expireLevel, onApprove, onReject, onAuthorizeOnce, diff --git a/src/i18n/en/aiTool.ts b/src/i18n/en/aiTool.ts index 3a913f8..1af321a 100644 --- a/src/i18n/en/aiTool.ts +++ b/src/i18n/en/aiTool.ts @@ -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', diff --git a/src/i18n/zh-CN/aiTool.ts b/src/i18n/zh-CN/aiTool.ts index fe20908..288eb72 100644 --- a/src/i18n/zh-CN/aiTool.ts +++ b/src/i18n/zh-CN/aiTool.ts @@ -10,6 +10,8 @@ export default { executionSlow: '工具「{name}」执行较久,请稍候…', // 审批挂起时长提示(>2分钟橙色,>5分钟红色) waitTime: '⏱ 已等待 {time}', + // 审批到期倒计时(BUG-260624-02,与前端自动拒绝时刻对齐,≤60s 红色) + expireIn: '⏳ {time} 后超时自动拒绝', // 工具执行失败视觉标识(AR-6 下 status=completed,需红色区分于成功) executionFailed: '执行失败', executionFailedHint: '工具执行失败,详情见下方',