From 773ebb6ef6212edfd53746d9bf9540cb72211962 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com> Date: Tue, 30 Jun 2026 21:54:21 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E:=20=E5=B0=8F=E7=A8=8B?= =?UTF-8?q?=E5=BA=8F=E5=AE=A1=E6=89=B9=E8=B6=85=E6=97=B6=E8=AE=A1=E6=97=B6?= =?UTF-8?q?=E5=99=A8(5min=20=E8=87=AA=E5=8A=A8=E6=8B=92=E7=BB=9D=E5=85=9C?= =?UTF-8?q?=E5=BA=95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 对齐桌面端 aiShared.startApprovalTimer,补齐小程序缺失的审批超时机制 - AiApprovalRequired/AiDirAuthRequired 触发 startApprovalTimer(每条独立计时) - AiToolCallCompleted/AiApprovalResult 触发 clearApprovalTimer(审批落定) - 切会话/新建/断连/重连/终态触发 clearAllApprovalTimers(防跨会话污染) - 超时自动拒绝(approve(id,false))+ toast 提示用户 - 5min 阈值符合移动端使用习惯(桌面端 15min) --- apps/df-miniapp/src/composables/useAiChat.ts | 51 ++++++++++++++++++-- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/apps/df-miniapp/src/composables/useAiChat.ts b/apps/df-miniapp/src/composables/useAiChat.ts index 5fcd2f8..3a39810 100644 --- a/apps/df-miniapp/src/composables/useAiChat.ts +++ b/apps/df-miniapp/src/composables/useAiChat.ts @@ -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>() + +/** 启动审批超时计时器(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 } }) }