重构: 对话透明化 L1 + ③.2 审批拆分 + ⑥.4 @展开 + DAG 展示
修复 tool_result 压缩零效果(BUG-260628-01):单行/少行/JSON 大字符 串字段逃逸压缩的问题,基于实测数据(53/94 次迭代零节省)调参, 增加字符级保底截断,压缩有效率从 8% 提升至 ~85%+
This commit is contained in:
212
src/composables/ai/useToolApproval.ts
Normal file
212
src/composables/ai/useToolApproval.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
/**
|
||||
* ToolCard 审批状态机 — 从 ToolCard.vue 抽离的审批 UI 逻辑。
|
||||
*
|
||||
* 职责:审批按钮 loading 态、审批挂起时长显示(BUG-260624-02)、
|
||||
* 审批超时兜底 toast(UX-260617-14)、approve/reject/path_auth 三选项处理、
|
||||
* status 变化自动复位 + pending 计时。
|
||||
*
|
||||
* 不含 store IPC 调用(useAiApproval 负责)和模板渲染——纯状态管理。
|
||||
*/
|
||||
import { ref, reactive, computed, watch, onBeforeUnmount, type Ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useConfirm } from '@/composables/useConfirm'
|
||||
import { STREAM_TIMEOUT_MS } from '@/composables/ai/useAiStream'
|
||||
import type { AiToolCallInfo } from '@/api/types'
|
||||
|
||||
/**
|
||||
* 审批按钮 loading 超时兜底(B-260616-08)。
|
||||
* 后端回事件使 tc.status 离开 pending_approval → watch 复位。
|
||||
* 兜底计时到点复位 + toast 提示(对齐全局看门狗 STREAM_TIMEOUT_MS,后端无回执时给用户重试入口)。
|
||||
*/
|
||||
const APPROVE_LOADING_TIMEOUT_MS = STREAM_TIMEOUT_MS
|
||||
|
||||
/** AE-2025-05:High 风险工具白名单(后端 tool_registry.rs RiskLevel::High 对齐) */
|
||||
const HIGH_RISK_TOOLS = new Set<string>([
|
||||
'delete_task', 'delete_project', 'restore_project', 'purge_project', 'delete_file', 'run_command',
|
||||
'http_request',
|
||||
])
|
||||
|
||||
export function useToolApproval(tc: Ref<AiToolCallInfo>, emit: any) {
|
||||
const { t } = useI18n()
|
||||
const { confirmState, confirmDialog, answerConfirm } = useConfirm()
|
||||
|
||||
// ── B-260616-08: 审批 loading 态 ──
|
||||
const approving = ref(false)
|
||||
let approvingTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
// ── BUG-260624-02: 审批挂起时长显示 ──
|
||||
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'
|
||||
})
|
||||
|
||||
// ── UX-260617-14: 审批超时兜底 toast ──
|
||||
const approveToast = reactive({ visible: false, msg: '' })
|
||||
let _approveToastTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
function showApproveTimeoutToast(): void {
|
||||
approveToast.msg = t('aiTool.approveLoadingTimeout')
|
||||
approveToast.visible = true
|
||||
if (_approveToastTimer) clearTimeout(_approveToastTimer)
|
||||
_approveToastTimer = setTimeout(() => { approveToast.visible = false }, 3000)
|
||||
}
|
||||
|
||||
/** AE-2025-05:High 风险工具的二次确认文案(按操作类型) */
|
||||
function highRiskConfirmMsg(name: string): string {
|
||||
if (name === 'run_command') return t('aiTool.confirmHighExec')
|
||||
if (name === 'http_request') return t('aiTool.confirmHighHttp')
|
||||
if (name === 'delete_task' || name === 'delete_project' || name === 'purge_project' || name === 'delete_file' || name === 'restore_project') {
|
||||
return t('aiTool.confirmHighDelete')
|
||||
}
|
||||
return t('aiTool.confirmHighGeneric')
|
||||
}
|
||||
|
||||
/**
|
||||
* 批准按钮处理(risk 类专用)。
|
||||
* AE-2025-05:High 风险工具 → 先二次确认(useConfirm 弹层)。
|
||||
* loading + 超时兜底复用 B-260616-08 机制。
|
||||
*/
|
||||
async function onApprove() {
|
||||
if (approving.value) return // 防重入
|
||||
// AE-2025-05:High 风险 → 先二次确认
|
||||
if (HIGH_RISK_TOOLS.has(tc.value.name)) {
|
||||
const ok = await confirmDialog(highRiskConfirmMsg(tc.value.name))
|
||||
if (!ok) return
|
||||
}
|
||||
approving.value = true
|
||||
approvingTimer = setTimeout(() => {
|
||||
approving.value = false
|
||||
approvingTimer = null
|
||||
console.warn('[AI] 审批 loading 超时,后端可能未回执,已复位允许重试:', tc.value.id)
|
||||
showApproveTimeoutToast()
|
||||
}, APPROVE_LOADING_TIMEOUT_MS)
|
||||
emit('approve', { id: tc.value.id, approved: true })
|
||||
}
|
||||
|
||||
/**
|
||||
* 拒绝按钮处理(risk 类专用)。
|
||||
* loading + 超时兜底复用 B-260616-08 机制。拒绝不做 High 风险二次确认(无害,加确认反增摩擦)。
|
||||
*/
|
||||
async function onReject() {
|
||||
if (approving.value) return // 防重入
|
||||
approving.value = true
|
||||
approvingTimer = setTimeout(() => {
|
||||
approving.value = false
|
||||
approvingTimer = null
|
||||
console.warn('[AI] 审批 loading 超时,后端可能未回执,已复位允许重试:', tc.value.id)
|
||||
showApproveTimeoutToast()
|
||||
}, APPROVE_LOADING_TIMEOUT_MS)
|
||||
emit('approve', { id: tc.value.id, approved: false })
|
||||
}
|
||||
|
||||
/**
|
||||
* path_auth 审批链阶段3b:path 类「仅本次会话授权」处理。
|
||||
* 与 onApprove 同款 loading + 超时兜底,emit 带 decision='once'。
|
||||
* path 类不做 High 风险二次确认(路径授权是显式白名单写入,非破坏性操作,三选项本身就是用户明示决策)。
|
||||
*/
|
||||
async function onAuthorizeOnce() {
|
||||
if (approving.value) return
|
||||
approving.value = true
|
||||
approvingTimer = setTimeout(() => {
|
||||
approving.value = false
|
||||
approvingTimer = null
|
||||
console.warn('[AI] 路径授权 loading 超时,后端可能未回执,已复位允许重试:', tc.value.id)
|
||||
showApproveTimeoutToast()
|
||||
}, APPROVE_LOADING_TIMEOUT_MS)
|
||||
emit('approve', { id: tc.value.id, approved: false, decision: 'once' })
|
||||
}
|
||||
|
||||
/**
|
||||
* path_auth 审批链阶段3b:path 类「写持久白名单」处理。
|
||||
* 同 onAuthorizeOnce,decision='always'。
|
||||
*/
|
||||
async function onAuthorizeAlways() {
|
||||
if (approving.value) return
|
||||
approving.value = true
|
||||
approvingTimer = setTimeout(() => {
|
||||
approving.value = false
|
||||
approvingTimer = null
|
||||
console.warn('[AI] 路径授权 loading 超时,后端可能未回执,已复位允许重试:', tc.value.id)
|
||||
showApproveTimeoutToast()
|
||||
}, APPROVE_LOADING_TIMEOUT_MS)
|
||||
emit('approve', { id: tc.value.id, approved: false, decision: 'always' })
|
||||
}
|
||||
|
||||
/**
|
||||
* path_auth 审批链阶段3b:path 类「拒绝」处理。
|
||||
* 同 onAuthorizeOnce,decision='deny'。
|
||||
*/
|
||||
async function onDeny() {
|
||||
if (approving.value) return
|
||||
approving.value = true
|
||||
approvingTimer = setTimeout(() => {
|
||||
approving.value = false
|
||||
approvingTimer = null
|
||||
console.warn('[AI] 路径授权 loading 超时,后端可能未回执,已复位允许重试:', tc.value.id)
|
||||
showApproveTimeoutToast()
|
||||
}, APPROVE_LOADING_TIMEOUT_MS)
|
||||
emit('approve', { id: tc.value.id, approved: false, decision: 'deny' })
|
||||
}
|
||||
|
||||
// status 离开 pending_approval → 复位 approving(后端回执/转态时)。
|
||||
watch(() => tc.value.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()
|
||||
})
|
||||
|
||||
return {
|
||||
approving,
|
||||
approveToast,
|
||||
waitSecs,
|
||||
waitLevel,
|
||||
formatWaitTime,
|
||||
onApprove,
|
||||
onReject,
|
||||
onAuthorizeOnce,
|
||||
onAuthorizeAlways,
|
||||
onDeny,
|
||||
// 供模板 ConfirmDialog 绑定
|
||||
confirmState,
|
||||
answerConfirm,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user