feat: Implement incremental historical event loading and a centralized state synchronization mechanism, including stream reconnection, for the AgentAudit page.
Build and Push CodeReview / build (push) Waiting to run
Details
Build and Push CodeReview / build (push) Waiting to run
Details
This commit is contained in:
parent
05db656fd1
commit
62c42341c4
|
|
@ -574,6 +574,17 @@ Final Answer: {{"findings": [...], "summary": "..."}}"""
|
|||
# 重置空响应计数器
|
||||
self._empty_retry_count = 0
|
||||
|
||||
# 🔥 检查是否是 API 错误(从 BaseAgent.stream_llm_call 返回)
|
||||
if llm_output.startswith("[API_ERROR:"):
|
||||
# 提取错误类型和消息
|
||||
match = re.match(r"\[API_ERROR:(\w+)\]\s*(.*)", llm_output)
|
||||
if match:
|
||||
error_type = match.group(1)
|
||||
error_message = match.group(2)
|
||||
logger.error(f"[{self.name}] Fatal API error: {error_type} - {error_message}")
|
||||
await self.emit_event("error", f"LLM API 错误 ({error_type}): {error_message}")
|
||||
break
|
||||
|
||||
# 解析 LLM 响应
|
||||
step = self._parse_llm_response(llm_output)
|
||||
self._steps.append(step)
|
||||
|
|
|
|||
|
|
@ -1033,7 +1033,7 @@ class BaseAgent(ABC):
|
|||
|
||||
# 使用特殊前缀标记 API 错误,让调用方能够识别
|
||||
# 格式:[API_ERROR:error_type] user_message
|
||||
if error_type in ("rate_limit", "quota_exceeded", "authentication", "connection"):
|
||||
if error_type in ("rate_limit", "quota_exceeded", "authentication", "connection", "server_error"):
|
||||
accumulated = f"[API_ERROR:{error_type}] {user_message}"
|
||||
elif not accumulated:
|
||||
accumulated = f"[系统错误: {error_msg}] 请重新思考并输出你的决策。"
|
||||
|
|
|
|||
|
|
@ -331,6 +331,19 @@ Action Input: {{"参数": "值"}}
|
|||
await asyncio.sleep(5) # 等待 5 秒后重试
|
||||
continue
|
||||
|
||||
elif error_type == "server_error":
|
||||
# 服务器错误 (5xx) - 重试
|
||||
api_retry_count = getattr(self, '_api_retry_count', 0) + 1
|
||||
self._api_retry_count = api_retry_count
|
||||
if api_retry_count >= 3:
|
||||
logger.error(f"[{self.name}] Too many server errors, stopping")
|
||||
await self.emit_event("error", f"API 服务端错误重试次数过多: {error_message}")
|
||||
break
|
||||
logger.warning(f"[{self.name}] Server error, retrying ({api_retry_count}/3)")
|
||||
await self.emit_event("warning", f"API 服务端繁忙或异常,重试中 ({api_retry_count}/3)")
|
||||
await asyncio.sleep(10) # 等待 10 秒后重试(服务端错误通常需要更久恢复)
|
||||
continue
|
||||
|
||||
# 重置 API 重试计数器(成功获取响应后)
|
||||
self._api_retry_count = 0
|
||||
|
||||
|
|
|
|||
|
|
@ -509,6 +509,17 @@ Final Answer: [JSON格式的结果]"""
|
|||
# 重置空响应计数器
|
||||
self._empty_retry_count = 0
|
||||
|
||||
# 🔥 检查是否是 API 错误(从 BaseAgent.stream_llm_call 返回)
|
||||
if llm_output.startswith("[API_ERROR:"):
|
||||
# 提取错误类型和消息
|
||||
match = re.match(r"\[API_ERROR:(\w+)\]\s*(.*)", llm_output)
|
||||
if match:
|
||||
error_type = match.group(1)
|
||||
error_message = match.group(2)
|
||||
logger.error(f"[{self.name}] Fatal API error: {error_type} - {error_message}")
|
||||
await self.emit_event("error", f"LLM API 错误 ({error_type}): {error_message}")
|
||||
break
|
||||
|
||||
# 解析 LLM 响应
|
||||
step = self._parse_llm_response(llm_output)
|
||||
self._steps.append(step)
|
||||
|
|
|
|||
|
|
@ -718,6 +718,17 @@ class VerificationAgent(BaseAgent):
|
|||
|
||||
self._total_tokens += tokens_this_round
|
||||
|
||||
# 🔥 检查是否是 API 错误(从 BaseAgent.stream_llm_call 返回)
|
||||
if llm_output.startswith("[API_ERROR:"):
|
||||
# 提取错误类型和消息
|
||||
match = re.match(r"\[API_ERROR:(\w+)\]\s*(.*)", llm_output)
|
||||
if match:
|
||||
error_type = match.group(1)
|
||||
error_message = match.group(2)
|
||||
logger.error(f"[{self.name}] Fatal API error: {error_type} - {error_message}")
|
||||
await self.emit_event("error", f"LLM API 错误 ({error_type}): {error_message}")
|
||||
break
|
||||
|
||||
# 🔥 Handle empty LLM response to prevent loops
|
||||
if not llm_output or not llm_output.strip():
|
||||
logger.warning(f"[{self.name}] Empty LLM response in iteration {self._iteration}")
|
||||
|
|
|
|||
|
|
@ -251,6 +251,9 @@ class LiteLLMAdapter(BaseLLMAdapter):
|
|||
except litellm.exceptions.APIConnectionError as e:
|
||||
api_response = self._extract_api_response(e)
|
||||
raise LLMError(f"无法连接到 API 服务", self.config.provider, api_response=api_response)
|
||||
except (litellm.exceptions.ServiceUnavailableError, litellm.exceptions.InternalServerError) as e:
|
||||
api_response = self._extract_api_response(e)
|
||||
raise LLMError(f"API 服务暂时不可用 ({type(e).__name__})", self.config.provider, 503, api_response=api_response)
|
||||
except litellm.exceptions.APIError as e:
|
||||
api_response = self._extract_api_response(e)
|
||||
raise LLMError(f"API 错误", self.config.provider, getattr(e, 'status_code', None), api_response=api_response)
|
||||
|
|
@ -469,6 +472,17 @@ class LiteLLMAdapter(BaseLLMAdapter):
|
|||
"accumulated": accumulated_content,
|
||||
"usage": None,
|
||||
}
|
||||
except (litellm.exceptions.ServiceUnavailableError, litellm.exceptions.InternalServerError) as e:
|
||||
# 服务不可用 - 服务器端 5xx 错误
|
||||
logger.error(f"Stream server error ({type(e).__name__}): {e}")
|
||||
yield {
|
||||
"type": "error",
|
||||
"error_type": "server_error",
|
||||
"error": str(e),
|
||||
"user_message": f"API 服务暂时不可用 ({type(e).__name__})",
|
||||
"accumulated": accumulated_content,
|
||||
"usage": None,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
# 其他错误 - 检查是否是包装的速率限制错误
|
||||
|
|
|
|||
|
|
@ -146,23 +146,33 @@ function AgentAuditPageContent() {
|
|||
}, [loadAgentTree]);
|
||||
|
||||
// 🔥 NEW: 加载历史事件并转换为日志项
|
||||
const loadHistoricalEvents = useCallback(async () => {
|
||||
const loadHistoricalEvents = useCallback(async (isSync = false) => {
|
||||
if (!taskId) return 0;
|
||||
|
||||
// 🔥 防止重复加载历史事件
|
||||
if (hasLoadedHistoricalEventsRef.current) {
|
||||
// 🔥 只有在非同步模式下才检查是否已加载过(首屏加载)
|
||||
if (!isSync && hasLoadedHistoricalEventsRef.current) {
|
||||
console.log('[AgentAudit] Historical events already loaded, skipping');
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 如果是首屏加载,标记为已尝试加载
|
||||
if (!isSync) {
|
||||
hasLoadedHistoricalEventsRef.current = true;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`[AgentAudit] Fetching historical events for task ${taskId}...`);
|
||||
const events = await getAgentEvents(taskId, { limit: 500 });
|
||||
const currentSeq = lastEventSequenceRef.current;
|
||||
console.log(`[AgentAudit] Fetching historical events for task ${taskId} (after_sequence=${currentSeq})...`);
|
||||
|
||||
// 🔥 传递当前已知的最后序列号,实现增量加载
|
||||
const events = await getAgentEvents(taskId, {
|
||||
after_sequence: currentSeq,
|
||||
limit: 500
|
||||
});
|
||||
console.log(`[AgentAudit] Received ${events.length} events from API`);
|
||||
|
||||
if (events.length === 0) {
|
||||
console.log('[AgentAudit] No historical events found');
|
||||
console.log('[AgentAudit] No new historical events found');
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -629,8 +639,43 @@ function AgentAuditPageContent() {
|
|||
disconnectStreamRef.current = disconnectStream;
|
||||
}, [disconnectStream]);
|
||||
|
||||
// 🔥 Centralized Sync State Function
|
||||
const syncState = useCallback(async () => {
|
||||
if (!taskId) return;
|
||||
|
||||
console.log('[AgentAudit] Synchronizing state...');
|
||||
try {
|
||||
// 1. 同步任务基本信息、发现项和树结构
|
||||
await Promise.all([loadTask(), loadFindings(), loadAgentTree()]);
|
||||
|
||||
// 2. 增量同步日志完成状态或缺失日志
|
||||
await loadHistoricalEvents(true);
|
||||
|
||||
// 3. 如果连接断开且任务仍在运行,尝试重新连接流
|
||||
if (!isConnected && isRunning) {
|
||||
console.log('[AgentAudit] Stream disconnected while task running, attempting reconnect...');
|
||||
hasConnectedRef.current = false; // 允许重新连接
|
||||
connectStream();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[AgentAudit] Failed to sync state:', err);
|
||||
}
|
||||
}, [taskId, loadTask, loadFindings, loadAgentTree, loadHistoricalEvents, isConnected, isRunning, connectStream]);
|
||||
|
||||
// ============ Effects ============
|
||||
|
||||
// 🔥 Visibility Change Listener - 同步状态的主要触发器
|
||||
useEffect(() => {
|
||||
const handleVisibilityChange = () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
syncState();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
return () => document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
}, [syncState]);
|
||||
|
||||
// Status animation
|
||||
useEffect(() => {
|
||||
if (!isRunning) return;
|
||||
|
|
|
|||
Loading…
Reference in New Issue