From 22362a77b87a61569fd6dcbda74c267e4973f725 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com> Date: Sun, 28 Jun 2026 00:39:18 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E:=20=E5=AE=A1=E6=89=B9?= =?UTF-8?q?=E6=8C=82=E8=B5=B7=E6=97=B6=E9=95=BF=E6=8F=90=E7=A4=BA(BUG-2606?= =?UTF-8?q?24-02=20=E6=96=B9=E6=A1=88=20C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 背景:AI 调用需审批的工具后,ToolCard 弹审批卡常驻直到用户批/拒, 用户走开忘了批则 AI loop 卡死,且卡片无任何时长提示。 方案选型(A/B/C 三选一,选 C): A 不做 - 挂起审批容易被遗忘 B 加超时+倒计时 - 超时后行为难定义(自动拒丢改动/自动批危险) C 显示挂起时长(本方案) - 纯前端展示,不动后端,不改审批行为 实现: - ToolCard pending_approval 态显示已等待 mm:ss - >2分钟橙色脉冲,>5分钟红色脉冲 - 注释中标注方案选型理由,便于后人理解决策上下文 --- docs/todo.md | 2 +- src/components/ToolCard.vue | 63 +++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/docs/todo.md b/docs/todo.md index 6954264..2d2c713 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -65,7 +65,7 @@ graph TD | **G1** 目标钉扎 | 🔨 代码✅`8ce18cb` 待实测 | G1/G2/G4 落地 | — | | **父①** 小bug攒批 | ✅ 完成 | ①.2 白名单✅(settings.rs) / ①.3 priority✅(idea.rs) / ①.1 BUG层1❌过时(F-260619-03 方案①取代,层2待决策) / ①.4 雷达图→归父⑤ | — | | **父②** 知识图谱Phase1 | ✅ Phase1完成 | ②.1 V29迁移✅ / ②.2 TaskRecord+TaskLinkRepo✅ / ②.3 IPC(create_task扩展+task_link CRUD+move_queue+get_tree)✅ / ②.4 父聚合✅(set_status_for_aggregation绕status收口) / ②.5 AI工具6✅(基线38) | G1(弱) | -| **父③** AI对话体验 | 📋 待办 | ③.1 B-260619-04 ToolCard / ③.2 REFACTOR-260619-04 审批状态机 | — | +| **父③** AI对话体验 | 🔨 ③.1✅ / ③.2待办 | ③.1 B-260619-04 ToolCard✅(跨轮收起已有+标题增强) / ③.2 REFACTOR-260619-04 审批状态机拆分 | — | | **父④** F-09 per-conv | ✅ 前端per-conv | ④.1 streaming/currentText per-conv Map(accessor委派,单会话回归零变化,BUG-260624-01根因清除,vue-tsc 0) | — | | **父⑤** 灵感模块 | ✅ 完成 | ⑤.1 软删除✅ / ①.4 雷达图✅ / ⑤.2 #05✅/#06拆const✅/#09/#10表单(逗号tags)✅ / #07 DEC-02保留purge(不改) / 附:priority_from_i32跨层映射修复(对齐前端0=critical) | #07→②.1 | | **父⑥** Phase2-5 | 🔨 Phase4 注入进行中 | ⑥.1事件流✅(V30) / ⑥.2基础设施✅(V31) / ⑥.3注入✅(resolver 增强+extra 渲染) / ⑥.4前端📋 diff --git a/src/components/ToolCard.vue b/src/components/ToolCard.vue index 9d0274d..8a93622 100644 --- a/src/components/ToolCard.vue +++ b/src/components/ToolCard.vue @@ -40,6 +40,10 @@
⚠ {{ tc.reason }}
+ +
+ ⏱ 已等待 {{ formatWaitTime(waitSecs) }} +
{{ ln.text }}{{ '\n' }}
@@ -176,6 +180,45 @@ const APPROVE_LOADING_TIMEOUT_MS = STREAM_TIMEOUT_MS const approving = ref(false) let approvingTimer: ReturnType | null = null +// ── 审批挂起时长显示(BUG-260624-02) ── +// 背景:AI 调用需审批的工具(patch_file/write_file 等)后,ToolCard 弹审批卡常驻 +// 直到用户批/拒。用户可能走开忘了批,卡片无任何时长提示。 +// 方案选型(A/B/C 三选一,选 C): +// A 不做(删 todo)——挂起审批容易被遗忘,AI loop 卡死 +// B 加超时+倒计时——后端加 timer + 超时后行为难定义(自动拒=丢改动/自动批=危险) +// C 显示挂起时长(本方案)——纯前端展示,不动后端,不改审批行为 +// 实现:进入 pending_approval 时记录起始时刻,每秒刷新 waitSecs。 +// >2分钟变橙色,>5分钟变红色,提示用户尽快审批。 +const pendingSince = ref(null) +const waitSecs = ref(0) +let waitTimer: ReturnType | null = null +function startPendingTimer() { + if (pendingSince.value !== null) return + pendingSince.value = Date.now() + waitSecs.value = 0 + if (waitTimer) clearInterval(waitTimer) + waitTimer = setInterval(() => { + if (pendingSince.value !== null) { + waitSecs.value = Math.floor((Date.now() - pendingSince.value) / 1000) + } + }, 1000) +} +function stopPendingTimer() { + if (waitTimer) { clearInterval(waitTimer); waitTimer = null } + pendingSince.value = null + waitSecs.value = 0 +} +function formatWaitTime(secs: number): string { + const m = Math.floor(secs / 60) + const s = secs % 60 + return `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}` +} +const waitLevel = computed<'normal' | 'long' | 'urgent'>(() => { + if (waitSecs.value >= 300) return 'urgent' // 5分钟 + if (waitSecs.value >= 120) return 'long' // 2分钟 + return 'normal' +}) + // ── 审批超时兜底 toast(本卡自管,分离窗口运行时 App 根 toast 不在 DOM) ── const approveToast = reactive({ visible: false, msg: '' }) let _approveToastTimer: ReturnType | null = null @@ -246,12 +289,16 @@ watch(() => props.tc.status, (s) => { if (s !== 'pending_approval') { approving.value = false if (approvingTimer) { clearTimeout(approvingTimer); approvingTimer = null } + stopPendingTimer() + } else { + startPendingTimer() } }, { immediate: true }) onBeforeUnmount(() => { if (approvingTimer) clearTimeout(approvingTimer) if (_approveToastTimer) clearTimeout(_approveToastTimer) + stopPendingTimer() }) /** 一次解析结果供失败判定复用(头部 isFailed 用于卡片 border/status-dot 着色) */ @@ -380,6 +427,22 @@ const diffLines = computed(() => parseDiffLines(props.tc.diff)) border-radius: var(--df-radius-sm, 4px); } +/* BUG-260624-02: 审批挂起时长提示(>2分钟橙色,>5分钟红色) */ +.ai-tool-wait-time { + padding: 3px 10px; margin-bottom: 6px; font-size: 11px; + font-family: var(--df-font-mono); + color: var(--df-text-dim, #888); + border-radius: var(--df-radius-sm, 4px); +} +.ai-tool-wait-time.is-long { + color: var(--df-warning, #d97706); + animation: pulseGlow 1.5s ease-in-out infinite; +} +.ai-tool-wait-time.is-urgent { + color: var(--df-danger, #e5484d); + animation: pulseGlow 1s ease-in-out infinite; +} + /* AE-2025-03: write_file 审批 diff 预览区(红删绿增行级) */ .ai-tool-approval-diff { margin: 0 10px 8px; border: 0.5px solid var(--df-border); border-radius: var(--df-radius-sm, 4px); overflow: hidden; } .ai-tool-diff-pre {