feat: Phase 3 - parallel execution, progress reporting, result caching + AgentChat bug fixes

Phase 3 能力:
- DAG 并行执行 (workflow_engine): asyncio.gather 并行执行就绪节点
- Debate 并行 (orchestrator): for 循环改为 asyncio.gather
- 粒度进度上报 (workflow_engine + tasks + websocket): Redis 推送 + DB 降级
- 工具结果缓存 (tool_manager): 确定性工具默认开启缓存
- LLM 响应缓存 (core): messages[-4:] + model 哈希,5min TTL

AgentChat bug 修复 (Gitea #1-#5):
- #1 SSE 降级重复空消息: fallback POST 前移除占位消息
- #2 streamTimeout 泄漏: while 正常退出后 clearTimeout
- #3 loading 闪烁: final/error 事件中提前设 loading=false
- #4 SSE 事件类型对齐: 确认匹配,未知类型加 console.warn
- #5 retryMessage 流式残留: 重试时清理占位消息

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
renjianbo
2026-05-05 00:00:51 +08:00
parent f3cb35c460
commit 7e00b027d4
8 changed files with 605 additions and 345 deletions

View File

@@ -439,6 +439,7 @@ async function sendMessage() {
// 尝试 SSE 流式(带超时控制)
let usedStreaming = false
let placeholderIdx = -1
streamingActive.value = false
const abortController = new AbortController()
const streamTimeout = setTimeout(() => abortController.abort(), 60000)
@@ -462,8 +463,8 @@ async function sendMessage() {
role: 'assistant', content: '', timestamp: Date.now(),
steps: [], _traceOpen: true, iterations: 0, tool_calls_made: 0,
}
const idx = messages.value[key].push(msg) - 1
const currentMsg = messages.value[key][idx]
placeholderIdx = messages.value[key].push(msg) - 1
const currentMsg = messages.value[key][placeholderIdx]
const reader = resp.body.getReader()
const decoder = new TextDecoder()
@@ -532,14 +533,19 @@ async function sendMessage() {
sessionId.value[key] = data.session_id
}
streamingActive.value = false
loading.value = false
} else if (eventType === 'error') {
currentMsg.content = data.content || ''
currentMsg.status = 'error'
streamingActive.value = false
loading.value = false
} else {
console.warn('[AgentChat] 未知 SSE 事件类型:', eventType, data)
}
} catch { /* 跳过畸形事件 */ }
}
}
clearTimeout(streamTimeout)
}
} catch {
clearTimeout(streamTimeout)
@@ -549,6 +555,11 @@ async function sendMessage() {
}
if (!usedStreaming) {
// 移除 SSE 阶段残留的占位消息Issue #1
if (placeholderIdx >= 0 && placeholderIdx < messages.value[key].length) {
messages.value[key].splice(placeholderIdx, 1)
}
// 降级:标准 POST 请求
const fallbackEndpoint = currentAgentId.value
? `/api/v1/agent-chat/${currentAgentId.value}`
@@ -613,9 +624,11 @@ function retryMessage(idx: number) {
// 查找错误消息之前的最后一条用户消息
let userMsg = ''
let userIdx = -1
for (let i = idx - 1; i >= 0; i--) {
if (msgs[i].role === 'user') {
userMsg = msgs[i].content
userIdx = i
break
}
}
@@ -624,15 +637,15 @@ function retryMessage(idx: number) {
return
}
// 移除该错误消息及关联的用户消息
const removeIndices: number[] = []
for (let i = idx - 1; i >= 0; i--) {
if (msgs[i].role === 'user' && msgs[i].content === userMsg) {
// 收集需要移除的消息:用户消息 + 错误消息 + 中间的流式占位消息
const removeIndices: number[] = [userIdx, idx]
for (let i = userIdx + 1; i < idx; i++) {
const m = msgs[i]
// 流式占位消息assistant 角色,内容为空或有 steps 但无实质内容
if (m.role === 'assistant' && (!m.content || m.content === '') && (m.steps?.length || 0) > 0) {
removeIndices.push(i)
break
}
}
removeIndices.push(idx)
// 从后往前删除,避免 index 错乱
removeIndices.sort((a, b) => b - a)