新增: 小程序审批超时计时器(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)
|
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 彻底停) */
|
/** 清除看门狗(终态/挂起/断连触发,timer 彻底停) */
|
||||||
function clearWatchdog(): void {
|
function clearWatchdog(): void {
|
||||||
if (streamTimer) clearTimeout(streamTimer)
|
if (streamTimer) clearTimeout(streamTimer)
|
||||||
@@ -468,6 +506,8 @@ export function handleEvent(event: AiChatEvent): void {
|
|||||||
// 移除对应挂起审批(若有)
|
// 移除对应挂起审批(若有)
|
||||||
const idx = pendingApprovals.findIndex((p) => p.id === event.id)
|
const idx = pendingApprovals.findIndex((p) => p.id === event.id)
|
||||||
if (idx >= 0) pendingApprovals.splice(idx, 1)
|
if (idx >= 0) pendingApprovals.splice(idx, 1)
|
||||||
|
// 审批计时器:工具完成 → 清(审批已生效)
|
||||||
|
clearApprovalTimer(event.id)
|
||||||
// P2-E:工具执行完成 → 清回填标志(审批已生效执行,不再回填)
|
// P2-E:工具执行完成 → 清回填标志(审批已生效执行,不再回填)
|
||||||
if (lastApprovedTc.value && lastApprovedTc.value.id === event.id) {
|
if (lastApprovedTc.value && lastApprovedTc.value.id === event.id) {
|
||||||
lastApprovedTc.value = null
|
lastApprovedTc.value = null
|
||||||
@@ -495,6 +535,7 @@ export function handleEvent(event: AiChatEvent): void {
|
|||||||
pendingApprovals.push(info)
|
pendingApprovals.push(info)
|
||||||
}
|
}
|
||||||
clearWatchdog() // 挂起等审批,停看门狗(用户操作后续生成会 reset 重启)
|
clearWatchdog() // 挂起等审批,停看门狗(用户操作后续生成会 reset 重启)
|
||||||
|
startApprovalTimer(event.id, event.name) // 5min 超时自动拒绝兜底
|
||||||
const tc = findToolCall(event.id)
|
const tc = findToolCall(event.id)
|
||||||
if (tc) {
|
if (tc) {
|
||||||
tc.status = 'pending_approval'
|
tc.status = 'pending_approval'
|
||||||
@@ -511,6 +552,7 @@ export function handleEvent(event: AiChatEvent): void {
|
|||||||
}
|
}
|
||||||
const idx = pendingApprovals.findIndex((p) => p.id === event.id)
|
const idx = pendingApprovals.findIndex((p) => p.id === event.id)
|
||||||
if (idx >= 0) pendingApprovals.splice(idx, 1)
|
if (idx >= 0) pendingApprovals.splice(idx, 1)
|
||||||
|
clearApprovalTimer(event.id) // 审批结果落定 → 清计时器
|
||||||
// P2-E:审批结果确认(批准/拒绝均落定)→ 清回填标志(该 id 已定,不再回填)
|
// P2-E:审批结果确认(批准/拒绝均落定)→ 清回填标志(该 id 已定,不再回填)
|
||||||
if (lastApprovedTc.value && lastApprovedTc.value.id === event.id) {
|
if (lastApprovedTc.value && lastApprovedTc.value.id === event.id) {
|
||||||
lastApprovedTc.value = null
|
lastApprovedTc.value = null
|
||||||
@@ -520,6 +562,7 @@ export function handleEvent(event: AiChatEvent): void {
|
|||||||
|
|
||||||
case 'AiDirAuthRequired': {
|
case 'AiDirAuthRequired': {
|
||||||
clearWatchdog() // 挂起等授权,停看门狗
|
clearWatchdog() // 挂起等授权,停看门狗
|
||||||
|
startApprovalTimer(event.id, event.tool) // 5min 超时自动拒绝
|
||||||
// F-#95 P0-3:路径授权进 pendingApprovals(kind=path),chat 页渲染 once/always/deny 按钮
|
// F-#95 P0-3:路径授权进 pendingApprovals(kind=path),chat 页渲染 once/always/deny 按钮
|
||||||
// (替代旧 system 文案降级「请在桌面端处理」,miniapp 成轻量审批终端,治 loop 卡死)。
|
// (替代旧 system 文案降级「请在桌面端处理」,miniapp 成轻量审批终端,治 loop 卡死)。
|
||||||
const authInfo: AiToolCallInfo = {
|
const authInfo: AiToolCallInfo = {
|
||||||
@@ -578,7 +621,7 @@ export function handleEvent(event: AiChatEvent): void {
|
|||||||
generating.value = false
|
generating.value = false
|
||||||
agentRound.value = 0
|
agentRound.value = 0
|
||||||
clearWatchdog() // 终态:停看门狗
|
clearWatchdog() // 终态:停看门狗
|
||||||
pendingApprovals.splice(0, pendingApprovals.length)
|
pendingApprovals.splice(0, pendingApprovals.length); clearAllApprovalTimers()
|
||||||
// P2-E:审批失败回填 —— 刚 approve(approved=true)成功发出但 device 返 AiError(执行失败/已被
|
// P2-E:审批失败回填 —— 刚 approve(approved=true)成功发出但 device 返 AiError(执行失败/已被
|
||||||
// 桌面端处理),回填 pendingApprovals 让用户重试,避免 UI 误导「已批准」实际未生效。
|
// 桌面端处理),回填 pendingApprovals 让用户重试,避免 UI 误导「已批准」实际未生效。
|
||||||
if (lastApprovedTc.value) {
|
if (lastApprovedTc.value) {
|
||||||
@@ -935,7 +978,7 @@ function switchConversation(convId: string): void {
|
|||||||
lastApprovedTc.value = null // P2-E:切会话清审批回填标志(防跨会话误回填)
|
lastApprovedTc.value = null // P2-E:切会话清审批回填标志(防跨会话误回填)
|
||||||
generating.value = false
|
generating.value = false
|
||||||
// MINIAPP-03:清残留挂起状态,防旧会话审批卡/超时回调污染新会话视图(对齐 newConversation)。
|
// MINIAPP-03:清残留挂起状态,防旧会话审批卡/超时回调污染新会话视图(对齐 newConversation)。
|
||||||
pendingApprovals.splice(0, pendingApprovals.length)
|
pendingApprovals.splice(0, pendingApprovals.length); clearAllApprovalTimers()
|
||||||
clearWatchdog()
|
clearWatchdog()
|
||||||
|
|
||||||
// F-#95 扩展:发 load_messages 拉历史(device route_load_messages 读库 → AiMessageHistory 回流)。
|
// F-#95 扩展:发 load_messages 拉历史(device route_load_messages 读库 → AiMessageHistory 回流)。
|
||||||
@@ -1022,7 +1065,7 @@ function newConversation(): void {
|
|||||||
maxRoundsActive.value = false // P1-E:新会话清 MaxRounds 挂起
|
maxRoundsActive.value = false // P1-E:新会话清 MaxRounds 挂起
|
||||||
lastApprovedTc.value = null // P2-E:新会话清审批回填标志
|
lastApprovedTc.value = null // P2-E:新会话清审批回填标志
|
||||||
generating.value = false
|
generating.value = false
|
||||||
pendingApprovals.splice(0, pendingApprovals.length)
|
pendingApprovals.splice(0, pendingApprovals.length); clearAllApprovalTimers()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1044,7 +1087,7 @@ function syncOnConnect(): void {
|
|||||||
// req2:拉完整历史
|
// req2:拉完整历史
|
||||||
wsClient.send({ cmd: 'load_messages', args: { conversation_id: convId } })
|
wsClient.send({ cmd: 'load_messages', args: { conversation_id: convId } })
|
||||||
// req3:清本地 pending + 发 sync_pending 重推权威审批状态
|
// req3:清本地 pending + 发 sync_pending 重推权威审批状态
|
||||||
pendingApprovals.splice(0, pendingApprovals.length)
|
pendingApprovals.splice(0, pendingApprovals.length); clearAllApprovalTimers()
|
||||||
wsClient.send({ cmd: 'sync_pending', args: { conversation_id: convId } })
|
wsClient.send({ cmd: 'sync_pending', args: { conversation_id: convId } })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user