新增: 审批挂起时长提示(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:
@@ -65,7 +65,7 @@ graph TD
|
|||||||
| **G1** 目标钉扎 | 🔨 代码✅`8ce18cb` 待实测 | G1/G2/G4 落地 | — |
|
| **G1** 目标钉扎 | 🔨 代码✅`8ce18cb` 待实测 | G1/G2/G4 落地 | — |
|
||||||
| **父①** 小bug攒批 | ✅ 完成 | ①.2 白名单✅(settings.rs) / ①.3 priority✅(idea.rs) / ①.1 BUG层1❌过时(F-260619-03 方案①取代,层2待决策) / ①.4 雷达图→归父⑤ | — |
|
| **父①** 小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(弱) |
|
| **父②** 知识图谱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) | — |
|
| **父④** 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 |
|
| **父⑤** 灵感模块 | ✅ 完成 | ⑤.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前端📋
|
| **父⑥** Phase2-5 | 🔨 Phase4 注入进行中 | ⑥.1事件流✅(V30) / ⑥.2基础设施✅(V31) / ⑥.3注入✅(resolver 增强+extra 渲染) / ⑥.4前端📋
|
||||||
|
|||||||
@@ -40,6 +40,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="tc.reason" class="ai-tool-approval-reason">⚠ {{ tc.reason }}</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 -->
|
<!-- AE-2025-03: write_file 审批 diff 预览(红删绿增,行级)。旧文件不存在→tc.diff 为空,回退显上方 args content -->
|
||||||
<div v-if="tc.diff" class="ai-tool-approval-diff">
|
<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>
|
<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)
|
const approving = ref(false)
|
||||||
let approvingTimer: ReturnType<typeof setTimeout> | null = null
|
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) ──
|
// ── 审批超时兜底 toast(本卡自管,分离窗口运行时 App 根 toast 不在 DOM) ──
|
||||||
const approveToast = reactive({ visible: false, msg: '' })
|
const approveToast = reactive({ visible: false, msg: '' })
|
||||||
let _approveToastTimer: ReturnType<typeof setTimeout> | null = null
|
let _approveToastTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
@@ -246,12 +289,16 @@ watch(() => props.tc.status, (s) => {
|
|||||||
if (s !== 'pending_approval') {
|
if (s !== 'pending_approval') {
|
||||||
approving.value = false
|
approving.value = false
|
||||||
if (approvingTimer) { clearTimeout(approvingTimer); approvingTimer = null }
|
if (approvingTimer) { clearTimeout(approvingTimer); approvingTimer = null }
|
||||||
|
stopPendingTimer()
|
||||||
|
} else {
|
||||||
|
startPendingTimer()
|
||||||
}
|
}
|
||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (approvingTimer) clearTimeout(approvingTimer)
|
if (approvingTimer) clearTimeout(approvingTimer)
|
||||||
if (_approveToastTimer) clearTimeout(_approveToastTimer)
|
if (_approveToastTimer) clearTimeout(_approveToastTimer)
|
||||||
|
stopPendingTimer()
|
||||||
})
|
})
|
||||||
|
|
||||||
/** 一次解析结果供失败判定复用(头部 isFailed 用于卡片 border/status-dot 着色) */
|
/** 一次解析结果供失败判定复用(头部 isFailed 用于卡片 border/status-dot 着色) */
|
||||||
@@ -380,6 +427,22 @@ const diffLines = computed(() => parseDiffLines(props.tc.diff))
|
|||||||
border-radius: var(--df-radius-sm, 4px);
|
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 预览区(红删绿增行级) */
|
/* 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-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 {
|
.ai-tool-diff-pre {
|
||||||
|
|||||||
Reference in New Issue
Block a user