- 窗口分离: FileExplorer 可弹出独立 Tauri 窗口 - 行号显示: 文件预览左侧显示行号列 - 按后缀图标: rs/ts/js/vue/css/html 等 20+ 类型彩色图标 - Diff 视图: 有 Git 变更的文件可切换 diff 红绿视图 - Git 变更面板: 变更文件列表(按目录缩进分组)+提交历史(分页加载) - 提交详情: 点击提交查看变更文件列表及文件级 diff - 时间显示: 提交历史显示相对时间/具体日期 - 面包屑导航不丢预览: 导航时不改变当前预览文件 - 刷新保留选中文件: 只清树缓存不清预览 - 中文编码修复: git 命令注入 LANG/LC_ALL 环境变量 - 自适应布局: 弹性 flex 布局,窄窗口适配 - 滚动条优化: 内容区内部滚动,不溢出页面层 - 多工程管理: 工具栏添加工程按钮+弹窗表单 - 审批加载超时缩短: 130s 降至 30s - 文件变更自动刷新: write_file/patch_file 触发 df-data-changed
213 lines
7.7 KiB
TypeScript
213 lines
7.7 KiB
TypeScript
/**
|
|
* 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 type { AiToolCallInfo } from '@/api/types'
|
|
|
|
/**
|
|
* 审批按钮 loading 超时兜底(B-260616-08)。
|
|
* 后端回事件使 tc.status 离开 pending_approval → watch 复位。
|
|
* 兜底计时到点复位 + toast 提示(后端 IPC 应在 1~3s 内返回;30s 兜底远足超时后允许用户重试)。
|
|
* 与 stream 看门狗(130s)解耦:审批是按钮级交互,不是后台流,不需要对齐后端 120s 空闲超时。
|
|
*/
|
|
const APPROVE_LOADING_TIMEOUT_MS = 30_000
|
|
|
|
/** 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,
|
|
}
|
|
}
|