From 3f75c966c2302c063da307fd0dbb54e927bfce40 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?cls=5F=E5=AE=81=E6=B3=A2=E6=9C=AC=E6=9C=BA?=
<908705107@qq.com>
Date: Sun, 14 Jun 2026 14:46:24 +0800
Subject: [PATCH] feat: improve agent workflow observability
---
.gitignore | 3 +
backend-fastapi/ai_platform/api/agent_api.py | 19 ++-
.../ai_platform/services/agent_service.py | 3 +
.../src/components/ChatBox/MessageBubble.vue | 20 ++-
.../web-ele/src/components/ChatBox/types.ts | 10 +-
.../composables/useEventHandler.ts | 136 ++++++++++++++++++
web/apps/web-ele/src/router/access.ts | 7 -
7 files changed, 187 insertions(+), 11 deletions(-)
diff --git a/.gitignore b/.gitignore
index a76bbf7..2cfafb5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,6 +23,9 @@ coverage
./web/package-lock.json
.VSCodeCounter
**/backend-mock/data
+/.tmp/
+/ai-agent-admin-dist-*.tgz
+/ai-agent-admin-web-dist*.tgz
# local env files
./web/.env.local
diff --git a/backend-fastapi/ai_platform/api/agent_api.py b/backend-fastapi/ai_platform/api/agent_api.py
index 9365617..9859b9f 100644
--- a/backend-fastapi/ai_platform/api/agent_api.py
+++ b/backend-fastapi/ai_platform/api/agent_api.py
@@ -422,8 +422,23 @@ async def publish_agent(agent_id: str, db: AsyncSession = Depends(get_db)):
# 验证必要配置
if agent.mode == "autonomous":
- if not agent.model_id:
- raise HTTPException(status_code=400, detail="请先配置模型")
+ if agent.model_id:
+ model_result = await db.execute(
+ select(LLMModel).where(
+ LLMModel.id == agent.model_id,
+ LLMModel.is_deleted == False,
+ )
+ )
+ model = model_result.scalar_one_or_none()
+ if not model:
+ raise HTTPException(status_code=400, detail="模型不存在")
+ if not model.is_active:
+ raise HTTPException(status_code=400, detail="模型已禁用")
+ elif not await _resolve_default_chat_model_id(db):
+ raise HTTPException(
+ status_code=400,
+ detail="请先在模型配置中启用一个 chat 模型,或为智能体指定模型",
+ )
elif agent.mode == "dialog_flow":
if not agent.workflow_id:
raise HTTPException(status_code=400, detail="对话流模式需要配置工作流")
diff --git a/backend-fastapi/ai_platform/services/agent_service.py b/backend-fastapi/ai_platform/services/agent_service.py
index dd3fa2e..46e1562 100644
--- a/backend-fastapi/ai_platform/services/agent_service.py
+++ b/backend-fastapi/ai_platform/services/agent_service.py
@@ -582,6 +582,9 @@ class AgentService:
'type': 'complete',
'elapsed_time': elapsed_time,
'tokens_used': total_tokens,
+ 'total_tokens': total_tokens,
+ 'conversation_id': str(conversation.id),
+ 'message_id': str(assistant_msg.id),
}
except Exception as e:
diff --git a/web/apps/web-ele/src/components/ChatBox/MessageBubble.vue b/web/apps/web-ele/src/components/ChatBox/MessageBubble.vue
index b0cc209..947dd66 100644
--- a/web/apps/web-ele/src/components/ChatBox/MessageBubble.vue
+++ b/web/apps/web-ele/src/components/ChatBox/MessageBubble.vue
@@ -150,6 +150,14 @@ const getStepLabel = (step: ReasoningStep) => {
tool_result: '工具结果',
knowledge_retrieval: '知识库检索',
annotation_reply: '标注回复',
+ parallel_start: '并行中',
+ parallel_complete: '并行完成',
+ waiting_input: '等待输入',
+ error: '失败',
+ loop_iteration_start: '循环中',
+ loop_iteration_complete: '循环完成',
+ loop_iteration_error: '循环失败',
+ loop_complete: '循环完成',
};
return labels[step.type] || step.type;
};
@@ -172,6 +180,14 @@ const getStepColor = (step: ReasoningStep) => {
tool_result: 'text-teal-500',
knowledge_retrieval: 'text-cyan-500',
annotation_reply: 'text-emerald-500',
+ parallel_start: 'text-indigo-500',
+ parallel_complete: 'text-emerald-500',
+ waiting_input: 'text-amber-500',
+ error: 'text-red-500',
+ loop_iteration_start: 'text-blue-500',
+ loop_iteration_complete: 'text-emerald-500',
+ loop_iteration_error: 'text-red-500',
+ loop_complete: 'text-emerald-500',
};
return colors[step.type] || 'text-gray-500';
};
@@ -301,7 +317,9 @@ const formatVoiceDuration = (seconds: number) => {
{{ getStepLabel(step) }}
- {{ step.content }}
+
+ {{ step.content }}
+
diff --git a/web/apps/web-ele/src/components/ChatBox/types.ts b/web/apps/web-ele/src/components/ChatBox/types.ts
index 065e34d..f462b5a 100644
--- a/web/apps/web-ele/src/components/ChatBox/types.ts
+++ b/web/apps/web-ele/src/components/ChatBox/types.ts
@@ -41,13 +41,21 @@ export interface ReasoningStep {
type:
| 'action'
| 'annotation_reply'
+ | 'error'
| 'knowledge_retrieval'
+ | 'loop_complete'
+ | 'loop_iteration_complete'
+ | 'loop_iteration_error'
+ | 'loop_iteration_start'
| 'node_complete'
| 'node_start'
| 'observation'
+ | 'parallel_complete'
+ | 'parallel_start'
| 'thought'
| 'tool_call'
- | 'tool_result';
+ | 'tool_result'
+ | 'waiting_input';
content: string;
tool?: string;
params?: Record;
diff --git a/web/apps/web-ele/src/components/ai-chat-panel/composables/useEventHandler.ts b/web/apps/web-ele/src/components/ai-chat-panel/composables/useEventHandler.ts
index 4865e6e..fb9b08c 100644
--- a/web/apps/web-ele/src/components/ai-chat-panel/composables/useEventHandler.ts
+++ b/web/apps/web-ele/src/components/ai-chat-panel/composables/useEventHandler.ts
@@ -164,6 +164,12 @@ export function useEventHandler(config: EventHandlerConfig) {
return fallbackLabel || nodeType || '';
};
+ const getItemCount = (value: any): number => {
+ if (Array.isArray(value)) return value.length;
+ if (value && typeof value === 'object') return Object.keys(value).length;
+ return 0;
+ };
+
/** 处理流式事件 */
const handleStreamEvent = (event: StreamEvent, msgId: string) => {
switch (event.type) {
@@ -272,9 +278,18 @@ export function useEventHandler(config: EventHandlerConfig) {
}
case 'error': {
+ currentSteps.value.push({
+ type: 'error',
+ content: event.message || event.content || '执行失败',
+ node_id: event.node_id,
+ node_type: event.node_type,
+ status: 'completed',
+ timestamp: new Date().toISOString(),
+ });
updateAssistantMessage(msgId, {
status: 'failed',
error_message: event.message || event.content || '执行失败',
+ reasoning_steps: [...currentSteps.value],
});
running.value = false;
break;
@@ -322,6 +337,19 @@ export function useEventHandler(config: EventHandlerConfig) {
outputs[outputVar] = loopEvent.loop_results;
}
+ currentSteps.value.push({
+ type: 'loop_complete',
+ content: '循环执行完成',
+ node_id: loopEvent.node_id,
+ node_type: 'loop',
+ output: outputs,
+ status: 'completed',
+ timestamp: new Date().toISOString(),
+ });
+ updateAssistantMessage(msgId, {
+ reasoning_steps: [...currentSteps.value],
+ });
+
if (props.enableNodeEvents) {
emit('node-complete', {
node_id: loopEvent.node_id,
@@ -336,6 +364,19 @@ export function useEventHandler(config: EventHandlerConfig) {
}
case 'loop_iteration_complete': {
+ currentSteps.value.push({
+ type: 'loop_iteration_complete',
+ content: `循环第 ${event.iteration || '-'} 次完成`,
+ node_id: event.node_id,
+ node_type: 'loop',
+ output: event.output,
+ status: 'completed',
+ timestamp: new Date().toISOString(),
+ });
+ updateAssistantMessage(msgId, {
+ reasoning_steps: [...currentSteps.value],
+ });
+
if (props.enableNodeEvents) {
emit('loop-iteration', {
node_id: event.node_id,
@@ -349,6 +390,18 @@ export function useEventHandler(config: EventHandlerConfig) {
}
case 'loop_iteration_error': {
+ currentSteps.value.push({
+ type: 'loop_iteration_error',
+ content: `循环第 ${event.iteration || '-'} 次失败:${event.error || '执行失败'}`,
+ node_id: event.node_id,
+ node_type: 'loop',
+ status: 'completed',
+ timestamp: new Date().toISOString(),
+ });
+ updateAssistantMessage(msgId, {
+ reasoning_steps: [...currentSteps.value],
+ });
+
if (props.enableNodeEvents) {
emit('loop-iteration', {
node_id: event.node_id,
@@ -362,6 +415,19 @@ export function useEventHandler(config: EventHandlerConfig) {
}
case 'loop_iteration_start': {
+ currentSteps.value.push({
+ type: 'loop_iteration_start',
+ content: `循环第 ${event.iteration || '-'} 次开始`,
+ node_id: event.node_id,
+ node_type: 'loop',
+ params: { item: event.item, total: event.total },
+ status: 'running',
+ timestamp: new Date().toISOString(),
+ });
+ updateAssistantMessage(msgId, {
+ reasoning_steps: [...currentSteps.value],
+ });
+
if (props.enableNodeEvents) {
emit('loop-iteration', {
node_id: event.node_id,
@@ -423,6 +489,60 @@ export function useEventHandler(config: EventHandlerConfig) {
break;
}
+ case 'parallel_complete': {
+ const existingIndex = currentSteps.value.findIndex(
+ (s) => s.node_id === event.node_id && s.type === 'parallel_start',
+ );
+ const branchResults = event.branch_results || {};
+ const content = `并行分支完成:${getItemCount(branchResults)} 个分支`;
+ if (existingIndex === -1) {
+ currentSteps.value.push({
+ type: 'parallel_complete',
+ content,
+ node_id: event.node_id,
+ node_type: 'parallel',
+ output: {
+ branch_results: branchResults,
+ total_tokens: event.total_tokens,
+ },
+ status: 'completed',
+ timestamp: new Date().toISOString(),
+ });
+ } else {
+ currentSteps.value[existingIndex] = {
+ ...currentSteps.value[existingIndex]!,
+ type: 'parallel_complete',
+ content,
+ output: {
+ branch_results: branchResults,
+ total_tokens: event.total_tokens,
+ },
+ status: 'completed',
+ };
+ }
+ updateAssistantMessage(msgId, {
+ reasoning_steps: [...currentSteps.value],
+ });
+ break;
+ }
+
+ case 'parallel_start': {
+ const branches = event.branches || [];
+ currentSteps.value.push({
+ type: 'parallel_start',
+ content: `并行分支开始:${getItemCount(branches)} 个分支`,
+ node_id: event.node_id,
+ node_type: 'parallel',
+ params: { branches },
+ status: 'running',
+ timestamp: new Date().toISOString(),
+ });
+ updateAssistantMessage(msgId, {
+ reasoning_steps: [...currentSteps.value],
+ });
+ break;
+ }
+
case 'node_event': {
const nodeEvent = event.event;
if (nodeEvent?.type === 'message') {
@@ -493,12 +613,28 @@ export function useEventHandler(config: EventHandlerConfig) {
waitingConfig.value = event.waiting_config || event.config;
const configData = event.waiting_config || (event as any).config;
+ currentSteps.value.push({
+ type: 'waiting_input',
+ content:
+ configData?.title ||
+ configData?.question ||
+ event.node_label ||
+ '等待用户输入',
+ node_id: event.node_id,
+ node_type: event.node_type,
+ params: configData,
+ status: 'running',
+ timestamp: new Date().toISOString(),
+ });
if (configData?.type === 'design_preview') {
handleDesignPreview(event, msgId, configData);
} else {
handleDialogFlowInteraction(msgId, configData);
}
+ updateAssistantMessage(msgId, {
+ reasoning_steps: [...currentSteps.value],
+ });
running.value = false;
break;
diff --git a/web/apps/web-ele/src/router/access.ts b/web/apps/web-ele/src/router/access.ts
index 2e662ce..7b67b91 100644
--- a/web/apps/web-ele/src/router/access.ts
+++ b/web/apps/web-ele/src/router/access.ts
@@ -205,13 +205,6 @@ async function generateAccess(options: GenerateMenuAndRoutesOptions) {
return await generateAccessible(preferences.app.accessMode, {
...options,
fetchMenuListAsync: async () => {
- const { ElMessage } =
- await import('element-plus/es/components/message/index');
- ElMessage({
- duration: 1500,
- message: `${$t('common.loadingMenu')}...`,
- });
-
const appContextStore = useAppContextStore();
const menus = appContextStore.appCode
? await getAllMenusApi(appContextStore.appCode)