fix(chat): prevent stale approval UI after SSE error/done

This commit is contained in:
matevip 2026-04-11 10:04:41 +08:00
parent f36c5b0aaa
commit 79c31ead32
3 changed files with 65 additions and 8 deletions

View File

@ -659,19 +659,19 @@ const segments = computed<MessageSegment[]>(() => {
const useSegmentedView = computed(() => segments.value.length > 1)
const toolCallsMeta = computed<ToolCallMeta[]>(() => {
return props.message.metadata?.toolCalls || []
return parsedMetadata.value?.toolCalls || []
})
const browserActionsMeta = computed<BrowserAction[]>(() => {
return props.message.metadata?.browserActions || []
return parsedMetadata.value?.browserActions || []
})
const planMeta = computed<PlanMeta | undefined>(() => {
return props.message.metadata?.plan
return parsedMetadata.value?.plan
})
const currentPhaseName = computed(() => {
const phase = props.message.metadata?.currentPhase
const phase = parsedMetadata.value?.currentPhase
switch (phase) {
case 'reasoning': return 'Reasoning'
case 'action': return 'Executing tools'
@ -693,7 +693,9 @@ const truncateArgs = (args: string) => {
// --- ---
const pendingApproval = computed(() => {
return props.message.metadata?.pendingApproval || null
const approval = parsedMetadata.value?.pendingApproval
if (!approval || approval.status === 'expired') return null
return approval
})
const approvalSeverityClass = computed(() => {
@ -724,7 +726,7 @@ const showExecutionPanel = computed(() => {
if (role.value !== 'assistant') return false
// execution panel
return toolCallsMeta.value.length > 0 || !!planMeta.value
|| (isGenerating.value && props.message.metadata?.currentPhase)
|| (isGenerating.value && parsedMetadata.value?.currentPhase)
|| !!pendingApproval.value
})

View File

@ -175,6 +175,44 @@ export function useChat(options: UseChatOptions): UseChatReturn {
return metadata
}
/**
* SSE error/done awaiting_approval UI
* updateMessage
*/
const expirePendingApprovals = (finalStatus: 'completed' | 'failed' | 'stopped') => {
for (const m of messages.value) {
if (m.role !== 'assistant') continue
const metadata = parseMetadata((m as any).metadata)
const pendingApproval = metadata?.pendingApproval
const hasPendingApproval = pendingApproval?.status === 'pending_approval'
const isAwaitingApprovalMsg = m.status === 'awaiting_approval' || metadata?.currentPhase === 'awaiting_approval'
if (!hasPendingApproval && !isAwaitingApprovalMsg) continue
if (m.id === undefined || m.id === null) continue
const toolCalls = Array.isArray(metadata?.toolCalls)
? metadata.toolCalls.map((tc: any) => (
tc?.status === 'running' || tc?.status === 'awaiting_approval'
? { ...tc, status: 'completed' }
: tc
))
: metadata?.toolCalls
updateMessage(m.id, {
...m,
status: m.status === 'awaiting_approval' ? finalStatus : m.status,
metadata: {
...metadata,
currentPhase: undefined,
runningToolName: undefined,
toolCalls,
pendingApproval: hasPendingApproval
? { ...pendingApproval, status: 'expired' }
: pendingApproval,
},
} as any)
}
}
// 消息管理
const {
messages,
@ -384,6 +422,7 @@ export function useChat(options: UseChatOptions): UseChatReturn {
: data.status === 'stopped' ? 'stopped' : 'completed'
if (data.status !== 'awaiting_approval') {
phaseInfo.value = null
expirePendingApprovals(data.status === 'stopped' ? 'stopped' : 'completed')
}
// 兜底清理排队状态(如果 queued_input_started 已经处理了则这里是 no-op
@ -431,6 +470,7 @@ export function useChat(options: UseChatOptions): UseChatReturn {
phaseInfo.value = null
// 错误时清理排队状态,避免脏残留
messageQueue.clear()
expirePendingApprovals('failed')
if (errorFired) return
errorFired = true

View File

@ -93,6 +93,9 @@ function mergeContentParts(localParts: any[], fetchedParts: any[]): any[] {
return merged.sort((a, b) => (order[a.type] ?? 99) - (order[b.type] ?? 99))
}
/** 审批的已决状态(前端通过 SSE 实时更新,比后端持久化更新) */
const RESOLVED_APPROVAL = new Set(['expired', 'approved', 'denied'])
function mergeMetadata(localMetaRaw: any, fetchedMetaRaw: any): Record<string, any> {
const localMeta = safeParseMeta(localMetaRaw)
const fetchedMeta = safeParseMeta(fetchedMetaRaw)
@ -114,7 +117,16 @@ function mergeMetadata(localMetaRaw: any, fetchedMetaRaw: any): Record<string, a
merged.toolCalls = fetchedToolCalls
}
if (!merged.pendingApproval && localMeta.pendingApproval) {
// pendingApproval前端已决/过期状态不被后端的 pending 状态回退
const localApprovalStatus = localMeta.pendingApproval?.status
if (RESOLVED_APPROVAL.has(localApprovalStatus)) {
merged.pendingApproval = localMeta.pendingApproval
// 清除已过期审批关联的 phase 字段,防止 UI 残留
if (localApprovalStatus === 'expired') {
delete merged.currentPhase
delete merged.runningToolName
}
} else if (!merged.pendingApproval && localMeta.pendingApproval) {
merged.pendingApproval = localMeta.pendingApproval
}
@ -142,7 +154,10 @@ function mergeAssistantMessages(localMsg: Message, fetchedMsg: Message): Message
: localMsg.content,
contentParts,
metadata: mergeMetadata(localMsg.metadata, fetchedMsg.metadata),
status: fetchedMsg.status || localMsg.status,
// 不要把已终结的状态failed/completed/stopped回退为 awaiting_approval
status: (fetchedMsg.status === 'awaiting_approval' && localMsg.status && localMsg.status !== 'awaiting_approval')
? localMsg.status
: (fetchedMsg.status || localMsg.status),
promptTokens: fetchedMsg.promptTokens ?? localMsg.promptTokens,
completionTokens: fetchedMsg.completionTokens ?? localMsg.completionTokens,
createTime: fetchedMsg.createTime || localMsg.createTime,