mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 20:08:18 +08:00
fix(chat): prevent stale approval UI after SSE error/done
This commit is contained in:
parent
f36c5b0aaa
commit
79c31ead32
@ -659,19 +659,19 @@ const segments = computed<MessageSegment[]>(() => {
|
|||||||
const useSegmentedView = computed(() => segments.value.length > 1)
|
const useSegmentedView = computed(() => segments.value.length > 1)
|
||||||
|
|
||||||
const toolCallsMeta = computed<ToolCallMeta[]>(() => {
|
const toolCallsMeta = computed<ToolCallMeta[]>(() => {
|
||||||
return props.message.metadata?.toolCalls || []
|
return parsedMetadata.value?.toolCalls || []
|
||||||
})
|
})
|
||||||
|
|
||||||
const browserActionsMeta = computed<BrowserAction[]>(() => {
|
const browserActionsMeta = computed<BrowserAction[]>(() => {
|
||||||
return props.message.metadata?.browserActions || []
|
return parsedMetadata.value?.browserActions || []
|
||||||
})
|
})
|
||||||
|
|
||||||
const planMeta = computed<PlanMeta | undefined>(() => {
|
const planMeta = computed<PlanMeta | undefined>(() => {
|
||||||
return props.message.metadata?.plan
|
return parsedMetadata.value?.plan
|
||||||
})
|
})
|
||||||
|
|
||||||
const currentPhaseName = computed(() => {
|
const currentPhaseName = computed(() => {
|
||||||
const phase = props.message.metadata?.currentPhase
|
const phase = parsedMetadata.value?.currentPhase
|
||||||
switch (phase) {
|
switch (phase) {
|
||||||
case 'reasoning': return 'Reasoning'
|
case 'reasoning': return 'Reasoning'
|
||||||
case 'action': return 'Executing tools'
|
case 'action': return 'Executing tools'
|
||||||
@ -693,7 +693,9 @@ const truncateArgs = (args: string) => {
|
|||||||
|
|
||||||
// --- 审批面板 ---
|
// --- 审批面板 ---
|
||||||
const pendingApproval = computed(() => {
|
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(() => {
|
const approvalSeverityClass = computed(() => {
|
||||||
@ -724,7 +726,7 @@ const showExecutionPanel = computed(() => {
|
|||||||
if (role.value !== 'assistant') return false
|
if (role.value !== 'assistant') return false
|
||||||
// 审批卡片有独立的渲染区域,但 execution panel 也应该在审批阶段展示上下文
|
// 审批卡片有独立的渲染区域,但 execution panel 也应该在审批阶段展示上下文
|
||||||
return toolCallsMeta.value.length > 0 || !!planMeta.value
|
return toolCallsMeta.value.length > 0 || !!planMeta.value
|
||||||
|| (isGenerating.value && props.message.metadata?.currentPhase)
|
|| (isGenerating.value && parsedMetadata.value?.currentPhase)
|
||||||
|| !!pendingApproval.value
|
|| !!pendingApproval.value
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -175,6 +175,44 @@ export function useChat(options: UseChatOptions): UseChatReturn {
|
|||||||
return metadata
|
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 {
|
const {
|
||||||
messages,
|
messages,
|
||||||
@ -384,6 +422,7 @@ export function useChat(options: UseChatOptions): UseChatReturn {
|
|||||||
: data.status === 'stopped' ? 'stopped' : 'completed'
|
: data.status === 'stopped' ? 'stopped' : 'completed'
|
||||||
if (data.status !== 'awaiting_approval') {
|
if (data.status !== 'awaiting_approval') {
|
||||||
phaseInfo.value = null
|
phaseInfo.value = null
|
||||||
|
expirePendingApprovals(data.status === 'stopped' ? 'stopped' : 'completed')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 兜底清理排队状态(如果 queued_input_started 已经处理了则这里是 no-op)
|
// 兜底清理排队状态(如果 queued_input_started 已经处理了则这里是 no-op)
|
||||||
@ -431,6 +470,7 @@ export function useChat(options: UseChatOptions): UseChatReturn {
|
|||||||
phaseInfo.value = null
|
phaseInfo.value = null
|
||||||
// 错误时清理排队状态,避免脏残留
|
// 错误时清理排队状态,避免脏残留
|
||||||
messageQueue.clear()
|
messageQueue.clear()
|
||||||
|
expirePendingApprovals('failed')
|
||||||
|
|
||||||
if (errorFired) return
|
if (errorFired) return
|
||||||
errorFired = true
|
errorFired = true
|
||||||
|
|||||||
@ -93,6 +93,9 @@ function mergeContentParts(localParts: any[], fetchedParts: any[]): any[] {
|
|||||||
return merged.sort((a, b) => (order[a.type] ?? 99) - (order[b.type] ?? 99))
|
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> {
|
function mergeMetadata(localMetaRaw: any, fetchedMetaRaw: any): Record<string, any> {
|
||||||
const localMeta = safeParseMeta(localMetaRaw)
|
const localMeta = safeParseMeta(localMetaRaw)
|
||||||
const fetchedMeta = safeParseMeta(fetchedMetaRaw)
|
const fetchedMeta = safeParseMeta(fetchedMetaRaw)
|
||||||
@ -114,7 +117,16 @@ function mergeMetadata(localMetaRaw: any, fetchedMetaRaw: any): Record<string, a
|
|||||||
merged.toolCalls = fetchedToolCalls
|
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
|
merged.pendingApproval = localMeta.pendingApproval
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -142,7 +154,10 @@ function mergeAssistantMessages(localMsg: Message, fetchedMsg: Message): Message
|
|||||||
: localMsg.content,
|
: localMsg.content,
|
||||||
contentParts,
|
contentParts,
|
||||||
metadata: mergeMetadata(localMsg.metadata, fetchedMsg.metadata),
|
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,
|
promptTokens: fetchedMsg.promptTokens ?? localMsg.promptTokens,
|
||||||
completionTokens: fetchedMsg.completionTokens ?? localMsg.completionTokens,
|
completionTokens: fetchedMsg.completionTokens ?? localMsg.completionTokens,
|
||||||
createTime: fetchedMsg.createTime || localMsg.createTime,
|
createTime: fetchedMsg.createTime || localMsg.createTime,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user