新增: 审批挂起时长提示(BUG-260624-02 方案 C)
背景:AI 调用需审批的工具后,ToolCard 弹审批卡常驻直到用户批/拒, 用户走开忘了批则 AI loop 卡死,且卡片无任何时长提示。 方案选型(A/B/C 三选一,选 C): A 不做 - 挂起审批容易被遗忘 B 加超时+倒计时 - 超时后行为难定义(自动拒丢改动/自动批危险) C 显示挂起时长(本方案) - 纯前端展示,不动后端,不改审批行为 实现: - ToolCard pending_approval 态显示已等待 mm:ss - >2分钟橙色脉冲,>5分钟红色脉冲 - 注释中标注方案选型理由,便于后人理解决策上下文
This commit is contained in:
@@ -40,6 +40,10 @@
|
||||
</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">
|
||||
⏱ 已等待 {{ formatWaitTime(waitSecs) }}
|
||||
</div>
|
||||
<!-- AE-2025-03: write_file 审批 diff 预览(红删绿增,行级)。旧文件不存在→tc.diff 为空,回退显上方 args content -->
|
||||
<div v-if="tc.diff" class="ai-tool-approval-diff">
|
||||
<pre class="ai-tool-diff-pre"><code><span v-for="(ln, idx) in diffLines" :key="idx" class="ai-tool-diff-line" :class="'ai-tool-diff-line--' + ln.kind">{{ ln.text }}{{ '\n' }}</span></code></pre>
|
||||
@@ -176,6 +180,45 @@ const APPROVE_LOADING_TIMEOUT_MS = STREAM_TIMEOUT_MS
|
||||
const approving = ref(false)
|
||||
let approvingTimer: ReturnType<typeof setTimeout> | 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<number | null>(null)
|
||||
const waitSecs = ref(0)
|
||||
let waitTimer: ReturnType<typeof setInterval> | 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<typeof setTimeout> | 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 {
|
||||
|
||||
Reference in New Issue
Block a user