新增: 小程序审批超时计时器(5min 自动拒绝兜底)
- 对齐桌面端 aiShared.startApprovalTimer,补齐小程序缺失的审批超时机制 - AiApprovalRequired/AiDirAuthRequired 触发 startApprovalTimer(每条独立计时) - AiToolCallCompleted/AiApprovalResult 触发 clearApprovalTimer(审批落定) - 切会话/新建/断连/重连/终态触发 clearAllApprovalTimers(防跨会话污染) - 超时自动拒绝(approve(id,false))+ toast 提示用户 - 5min 阈值符合移动端使用习惯(桌面端 15min)
This commit is contained in:
@@ -215,6 +215,44 @@ function resetWatchdog(): void {
|
||||
streamTimer = setTimeout(onStreamTimeout, STREAM_TIMEOUT_MS)
|
||||
}
|
||||
|
||||
// ── 审批超时计时器(miniapp 补齐,对齐桌面 aiShared.startApprovalTimer)──
|
||||
// 治「审批永久挂起」:用户没在 5min 内处理审批 → 自动拒绝(对应桌面端 APPROVAL_TIMEOUT_DEFAULT_MS=900000,
|
||||
// miniapp 取 5min=300000 更符合移动端使用习惯)。每条审批独立计时,approve/拒绝/完成/切会话 清除。
|
||||
const APPROVAL_TIMEOUT_MS = 300_000 // 5 分钟
|
||||
const _approvalTimers = new Map<string, ReturnType<typeof setTimeout>>()
|
||||
|
||||
/** 启动审批超时计时器(AiApprovalRequired/AiDirAuthRequired 触发)。 */
|
||||
function startApprovalTimer(tcId: string, toolName: string): void {
|
||||
if (_approvalTimers.has(tcId)) return // 幂等
|
||||
const timer = setTimeout(() => {
|
||||
_approvalTimers.delete(tcId)
|
||||
// 超时自动拒绝(对齐桌面端:到点调 aiApi.approve(id,false))
|
||||
const idx = pendingApprovals.findIndex((p) => p.id === tcId)
|
||||
if (idx >= 0) {
|
||||
pendingApprovals.splice(idx, 1)
|
||||
uni.showToast({ title: `审批超时已自动拒绝:${toolName}`, icon: 'none' })
|
||||
// 发拒绝命令(device 收到后走 reject 链路,工具不执行,emit AiApprovalResult approved=false)
|
||||
approve(tcId, false)
|
||||
}
|
||||
}, APPROVAL_TIMEOUT_MS)
|
||||
_approvalTimers.set(tcId, timer)
|
||||
}
|
||||
|
||||
/** 清除审批超时计时器(approve/拒绝/完成/切会话/断连调用)。 */
|
||||
function clearApprovalTimer(tcId: string): void {
|
||||
const t = _approvalTimers.get(tcId)
|
||||
if (t) {
|
||||
clearTimeout(t)
|
||||
_approvalTimers.delete(tcId)
|
||||
}
|
||||
}
|
||||
|
||||
/** 清除所有审批计时器(切会话/断连/重连/终态)。 */
|
||||
function clearAllApprovalTimers(): void {
|
||||
for (const t of _approvalTimers.values()) clearTimeout(t)
|
||||
_approvalTimers.clear()
|
||||
}
|
||||
|
||||
/** 清除看门狗(终态/挂起/断连触发,timer 彻底停) */
|
||||
function clearWatchdog(): void {
|
||||
if (streamTimer) clearTimeout(streamTimer)
|
||||
@@ -468,6 +506,8 @@ export function handleEvent(event: AiChatEvent): void {
|
||||
// 移除对应挂起审批(若有)
|
||||
const idx = pendingApprovals.findIndex((p) => p.id === event.id)
|
||||
if (idx >= 0) pendingApprovals.splice(idx, 1)
|
||||
// 审批计时器:工具完成 → 清(审批已生效)
|
||||
clearApprovalTimer(event.id)
|
||||
// P2-E:工具执行完成 → 清回填标志(审批已生效执行,不再回填)
|
||||
if (lastApprovedTc.value && lastApprovedTc.value.id === event.id) {
|
||||
lastApprovedTc.value = null
|
||||
@@ -495,6 +535,7 @@ export function handleEvent(event: AiChatEvent): void {
|
||||
pendingApprovals.push(info)
|
||||
}
|
||||
clearWatchdog() // 挂起等审批,停看门狗(用户操作后续生成会 reset 重启)
|
||||
startApprovalTimer(event.id, event.name) // 5min 超时自动拒绝兜底
|
||||
const tc = findToolCall(event.id)
|
||||
if (tc) {
|
||||
tc.status = 'pending_approval'
|
||||
@@ -511,6 +552,7 @@ export function handleEvent(event: AiChatEvent): void {
|
||||
}
|
||||
const idx = pendingApprovals.findIndex((p) => p.id === event.id)
|
||||
if (idx >= 0) pendingApprovals.splice(idx, 1)
|
||||
clearApprovalTimer(event.id) // 审批结果落定 → 清计时器
|
||||
// P2-E:审批结果确认(批准/拒绝均落定)→ 清回填标志(该 id 已定,不再回填)
|
||||
if (lastApprovedTc.value && lastApprovedTc.value.id === event.id) {
|
||||
lastApprovedTc.value = null
|
||||
@@ -520,6 +562,7 @@ export function handleEvent(event: AiChatEvent): void {
|
||||
|
||||
case 'AiDirAuthRequired': {
|
||||
clearWatchdog() // 挂起等授权,停看门狗
|
||||
startApprovalTimer(event.id, event.tool) // 5min 超时自动拒绝
|
||||
// F-#95 P0-3:路径授权进 pendingApprovals(kind=path),chat 页渲染 once/always/deny 按钮
|
||||
// (替代旧 system 文案降级「请在桌面端处理」,miniapp 成轻量审批终端,治 loop 卡死)。
|
||||
const authInfo: AiToolCallInfo = {
|
||||
@@ -578,7 +621,7 @@ export function handleEvent(event: AiChatEvent): void {
|
||||
generating.value = false
|
||||
agentRound.value = 0
|
||||
clearWatchdog() // 终态:停看门狗
|
||||
pendingApprovals.splice(0, pendingApprovals.length)
|
||||
pendingApprovals.splice(0, pendingApprovals.length); clearAllApprovalTimers()
|
||||
// P2-E:审批失败回填 —— 刚 approve(approved=true)成功发出但 device 返 AiError(执行失败/已被
|
||||
// 桌面端处理),回填 pendingApprovals 让用户重试,避免 UI 误导「已批准」实际未生效。
|
||||
if (lastApprovedTc.value) {
|
||||
@@ -935,7 +978,7 @@ function switchConversation(convId: string): void {
|
||||
lastApprovedTc.value = null // P2-E:切会话清审批回填标志(防跨会话误回填)
|
||||
generating.value = false
|
||||
// MINIAPP-03:清残留挂起状态,防旧会话审批卡/超时回调污染新会话视图(对齐 newConversation)。
|
||||
pendingApprovals.splice(0, pendingApprovals.length)
|
||||
pendingApprovals.splice(0, pendingApprovals.length); clearAllApprovalTimers()
|
||||
clearWatchdog()
|
||||
|
||||
// F-#95 扩展:发 load_messages 拉历史(device route_load_messages 读库 → AiMessageHistory 回流)。
|
||||
@@ -1022,7 +1065,7 @@ function newConversation(): void {
|
||||
maxRoundsActive.value = false // P1-E:新会话清 MaxRounds 挂起
|
||||
lastApprovedTc.value = null // P2-E:新会话清审批回填标志
|
||||
generating.value = false
|
||||
pendingApprovals.splice(0, pendingApprovals.length)
|
||||
pendingApprovals.splice(0, pendingApprovals.length); clearAllApprovalTimers()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1044,7 +1087,7 @@ function syncOnConnect(): void {
|
||||
// req2:拉完整历史
|
||||
wsClient.send({ cmd: 'load_messages', args: { conversation_id: convId } })
|
||||
// req3:清本地 pending + 发 sync_pending 重推权威审批状态
|
||||
pendingApprovals.splice(0, pendingApprovals.length)
|
||||
pendingApprovals.splice(0, pendingApprovals.length); clearAllApprovalTimers()
|
||||
wsClient.send({ cmd: 'sync_pending', args: { conversation_id: convId } })
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user