Build lightweight AI agent admin
This commit is contained in:
@@ -0,0 +1,515 @@
|
||||
/**
|
||||
* 流式事件处理器
|
||||
*
|
||||
* 统一处理工作流和 Agent 的流式事件
|
||||
*/
|
||||
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
import type {
|
||||
AiChatPanelEmits,
|
||||
AiChatPanelProps,
|
||||
ChatMessage,
|
||||
DesignPreviewData,
|
||||
ReasoningStep,
|
||||
StreamEvent,
|
||||
} from '../types';
|
||||
|
||||
/** 事件处理器配置 */
|
||||
interface EventHandlerConfig {
|
||||
props: AiChatPanelProps;
|
||||
emit: AiChatPanelEmits;
|
||||
messages: Ref<ChatMessage[]>;
|
||||
currentSteps: Ref<ReasoningStep[]>;
|
||||
currentRunId: Ref<string>;
|
||||
currentAssistantMsgId: Ref<null | string>;
|
||||
waitingForInput: Ref<boolean>;
|
||||
waitingConfig: Ref<any>;
|
||||
running: Ref<boolean>;
|
||||
conversationId: Ref<null | string>;
|
||||
// 设计预览回调
|
||||
onDesignPreview?: (data: DesignPreviewData) => void;
|
||||
}
|
||||
|
||||
/** 生成消息 ID */
|
||||
const generateId = () =>
|
||||
`msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
|
||||
/**
|
||||
* 创建事件处理器
|
||||
*/
|
||||
export function useEventHandler(config: EventHandlerConfig) {
|
||||
const {
|
||||
props,
|
||||
emit,
|
||||
messages,
|
||||
currentSteps,
|
||||
currentRunId,
|
||||
// currentAssistantMsgId 保留用于未来扩展
|
||||
waitingForInput,
|
||||
waitingConfig,
|
||||
running,
|
||||
onDesignPreview,
|
||||
} = config;
|
||||
|
||||
/** 更新助手消息 */
|
||||
const updateAssistantMessage = (
|
||||
msgId: string,
|
||||
updates: Partial<ChatMessage>,
|
||||
) => {
|
||||
const index = messages.value.findIndex((m) => m.id === msgId);
|
||||
if (index !== -1) {
|
||||
const currentMsg = messages.value[index];
|
||||
if (currentMsg) {
|
||||
messages.value[index] = { ...currentMsg, ...updates } as ChatMessage;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** 获取等待提示文本 */
|
||||
const getWaitingPrompt = (waitConfig: any): string => {
|
||||
if (!waitConfig) return '等待输入...';
|
||||
switch (waitConfig.type) {
|
||||
case 'choice': {
|
||||
return waitConfig.question || '请选择:';
|
||||
}
|
||||
case 'confirm': {
|
||||
return `**${waitConfig.title || '确认'}**\n\n${waitConfig.content || '请确认是否继续?'}`;
|
||||
}
|
||||
case 'question': {
|
||||
return waitConfig.question || '请输入:';
|
||||
}
|
||||
default: {
|
||||
return '等待输入...';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** 处理设计预览事件 */
|
||||
const handleDesignPreview = (
|
||||
event: StreamEvent,
|
||||
msgId: string,
|
||||
configData: any,
|
||||
) => {
|
||||
const previewType = configData.preview_type;
|
||||
const previewData: DesignPreviewData = {
|
||||
type: previewType,
|
||||
title: configData.title || '设计预览',
|
||||
data: configData.data || {},
|
||||
nodeId: event.node_id || '',
|
||||
form_fields: configData.form_fields,
|
||||
table_configs: configData.table_configs,
|
||||
layoutOptions: configData.layoutOptions,
|
||||
themeOptions: configData.themeOptions,
|
||||
};
|
||||
|
||||
// 调用设计预览回调
|
||||
onDesignPreview?.(previewData);
|
||||
|
||||
// 显示提示消息
|
||||
const waitingContent = `**${configData.title}**\n\n${configData.message || '请在右侧面板中确认或编辑设计'}`;
|
||||
const initialMsg = messages.value.find((m) => m.id === msgId);
|
||||
if (initialMsg && !initialMsg.content?.trim()) {
|
||||
updateAssistantMessage(msgId, {
|
||||
status: 'completed',
|
||||
content: waitingContent,
|
||||
});
|
||||
} else {
|
||||
const newMsg: ChatMessage = {
|
||||
id: generateId(),
|
||||
role: 'assistant',
|
||||
content: waitingContent,
|
||||
timestamp: new Date(),
|
||||
status: 'completed',
|
||||
};
|
||||
messages.value.push(newMsg);
|
||||
}
|
||||
};
|
||||
|
||||
/** 处理普通对话流交互 */
|
||||
const handleDialogFlowInteraction = (msgId: string, configData: any) => {
|
||||
const waitingContent = getWaitingPrompt(configData);
|
||||
|
||||
const initialMsg = messages.value.find((m) => m.id === msgId);
|
||||
if (initialMsg && !initialMsg.content?.trim()) {
|
||||
updateAssistantMessage(msgId, {
|
||||
status: 'completed',
|
||||
content: waitingContent,
|
||||
interaction: configData,
|
||||
});
|
||||
} else {
|
||||
const newMsg: ChatMessage = {
|
||||
id: generateId(),
|
||||
role: 'assistant',
|
||||
content: waitingContent,
|
||||
timestamp: new Date(),
|
||||
status: 'completed',
|
||||
interaction: configData,
|
||||
};
|
||||
messages.value.push(newMsg);
|
||||
}
|
||||
};
|
||||
|
||||
/** 获取节点显示名称(优先从前端 nodes 中获取) */
|
||||
const getNodeLabel = (
|
||||
nodeId?: string,
|
||||
nodeType?: string,
|
||||
fallbackLabel?: string,
|
||||
) => {
|
||||
if (!nodeId) return fallbackLabel || nodeType || '';
|
||||
if (props.nodes?.length) {
|
||||
const node = props.nodes.find((n) => n.id === nodeId);
|
||||
if (node?.data?.label) return node.data.label as string;
|
||||
}
|
||||
return fallbackLabel || nodeType || '';
|
||||
};
|
||||
|
||||
/** 处理流式事件 */
|
||||
const handleStreamEvent = (event: StreamEvent, msgId: string) => {
|
||||
switch (event.type) {
|
||||
// ========== Agent 特有事件 ==========
|
||||
case 'action': {
|
||||
currentSteps.value.push({
|
||||
type: 'action',
|
||||
content: '',
|
||||
tool: event.tool,
|
||||
params: event.params,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
updateAssistantMessage(msgId, {
|
||||
reasoning_steps: [...currentSteps.value],
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'answer': {
|
||||
const initialMsg = messages.value.find((m) => m.id === msgId);
|
||||
if (initialMsg && !initialMsg.content?.trim()) {
|
||||
updateAssistantMessage(msgId, {
|
||||
status: 'completed',
|
||||
content: event.content || '',
|
||||
});
|
||||
} else {
|
||||
const answerMsg: ChatMessage = {
|
||||
id: generateId(),
|
||||
role: 'assistant',
|
||||
content: event.content || '',
|
||||
timestamp: new Date(),
|
||||
status: 'completed',
|
||||
};
|
||||
messages.value.push(answerMsg);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'complete': {
|
||||
let endOutput = '';
|
||||
if (
|
||||
event.outputs &&
|
||||
typeof event.outputs.output === 'string' &&
|
||||
event.outputs.output.trim()
|
||||
) {
|
||||
endOutput = event.outputs.output;
|
||||
}
|
||||
|
||||
const initialMsg = messages.value.find((m) => m.id === msgId);
|
||||
const initialMsgHasContent = initialMsg?.content?.trim();
|
||||
|
||||
if (endOutput) {
|
||||
if (initialMsg && !initialMsgHasContent) {
|
||||
updateAssistantMessage(msgId, {
|
||||
status: 'completed',
|
||||
content: endOutput,
|
||||
elapsed_time: event.elapsed_time,
|
||||
tokens_used: event.tokens_used || event.total_tokens,
|
||||
});
|
||||
} else {
|
||||
const endMsg: ChatMessage = {
|
||||
id: generateId(),
|
||||
role: 'assistant',
|
||||
content: endOutput,
|
||||
timestamp: new Date(),
|
||||
status: 'completed',
|
||||
elapsed_time: event.elapsed_time,
|
||||
tokens_used: event.tokens_used || event.total_tokens,
|
||||
};
|
||||
messages.value.push(endMsg);
|
||||
}
|
||||
} else if (!initialMsgHasContent) {
|
||||
const otherAssistantMessages = messages.value.filter(
|
||||
(m) =>
|
||||
m.role === 'assistant' && m.id !== msgId && m.content?.trim(),
|
||||
);
|
||||
if (otherAssistantMessages.length > 0) {
|
||||
const initialMsgIndex = messages.value.findIndex(
|
||||
(m) => m.id === msgId,
|
||||
);
|
||||
if (initialMsgIndex !== -1) {
|
||||
messages.value.splice(initialMsgIndex, 1);
|
||||
}
|
||||
} else {
|
||||
updateAssistantMessage(msgId, {
|
||||
status: 'completed',
|
||||
elapsed_time: event.elapsed_time,
|
||||
tokens_used: event.tokens_used || event.total_tokens,
|
||||
content: '工作流执行完成',
|
||||
});
|
||||
}
|
||||
} else if (initialMsg) {
|
||||
// 流式输出已有内容,只更新状态和统计信息
|
||||
updateAssistantMessage(msgId, {
|
||||
status: 'completed',
|
||||
elapsed_time: event.elapsed_time,
|
||||
tokens_used: event.tokens_used || event.total_tokens,
|
||||
});
|
||||
}
|
||||
|
||||
running.value = false;
|
||||
|
||||
// 触发消息完成事件,用于刷新对话列表(更新标题)
|
||||
emit('message-complete', config.conversationId.value);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'error': {
|
||||
updateAssistantMessage(msgId, {
|
||||
status: 'failed',
|
||||
error_message: event.message || event.content || '执行失败',
|
||||
});
|
||||
running.value = false;
|
||||
break;
|
||||
}
|
||||
|
||||
case 'llm_chunk': {
|
||||
// 更新助手消息内容(流式输出)
|
||||
const initialMsg = messages.value.find((m) => m.id === msgId);
|
||||
if (initialMsg) {
|
||||
updateAssistantMessage(msgId, {
|
||||
content: event.accumulated_content || event.content || '',
|
||||
status: 'streaming',
|
||||
});
|
||||
} else {
|
||||
// 如果消息不存在,创建新消息
|
||||
const streamMsg: ChatMessage = {
|
||||
id: msgId,
|
||||
role: 'assistant',
|
||||
content: event.accumulated_content || event.content || '',
|
||||
timestamp: new Date(),
|
||||
status: 'streaming',
|
||||
};
|
||||
messages.value.push(streamMsg);
|
||||
}
|
||||
|
||||
if (props.enableNodeEvents) {
|
||||
emit('node-streaming', {
|
||||
node_id: event.node_id,
|
||||
content: event.content,
|
||||
accumulated_content: event.accumulated_content,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// ========== 循环事件 ==========
|
||||
case 'loop_complete': {
|
||||
const loopEvent = event as any;
|
||||
const outputVar = loopEvent.output_variable || 'loop_results';
|
||||
const outputs: Record<string, any> = {
|
||||
total_iterations: loopEvent.total_iterations,
|
||||
results_count: loopEvent.results_count,
|
||||
};
|
||||
if (loopEvent.loop_results) {
|
||||
outputs[outputVar] = loopEvent.loop_results;
|
||||
}
|
||||
|
||||
if (props.enableNodeEvents) {
|
||||
emit('node-complete', {
|
||||
node_id: loopEvent.node_id,
|
||||
node_type: 'loop',
|
||||
status: 'success',
|
||||
elapsed_time: 0,
|
||||
tokens_used: 0,
|
||||
outputs,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'loop_iteration_complete': {
|
||||
if (props.enableNodeEvents) {
|
||||
emit('loop-iteration', {
|
||||
node_id: event.node_id,
|
||||
type: 'complete',
|
||||
iteration: event.iteration,
|
||||
total: event.total,
|
||||
output: event.output,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'loop_iteration_error': {
|
||||
if (props.enableNodeEvents) {
|
||||
emit('loop-iteration', {
|
||||
node_id: event.node_id,
|
||||
type: 'error',
|
||||
iteration: event.iteration,
|
||||
total: event.total,
|
||||
error: event.error,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'loop_iteration_start': {
|
||||
if (props.enableNodeEvents) {
|
||||
emit('loop-iteration', {
|
||||
node_id: event.node_id,
|
||||
type: 'start',
|
||||
iteration: event.iteration,
|
||||
total: event.total,
|
||||
item: event.item,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'node_complete': {
|
||||
const existingIndex = currentSteps.value.findIndex(
|
||||
(s) => s.node_id === event.node_id && s.type === 'node_start',
|
||||
);
|
||||
const completeLabel = getNodeLabel(
|
||||
event.node_id,
|
||||
event.node_type,
|
||||
event.node_label,
|
||||
);
|
||||
if (existingIndex === -1) {
|
||||
currentSteps.value.push({
|
||||
type: 'node_complete',
|
||||
content: completeLabel,
|
||||
node_id: event.node_id,
|
||||
node_type: event.node_type,
|
||||
output: event.outputs,
|
||||
status: 'completed',
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
} else {
|
||||
currentSteps.value[existingIndex] = {
|
||||
...currentSteps.value[existingIndex]!,
|
||||
type: 'node_complete',
|
||||
content: completeLabel,
|
||||
output: event.outputs,
|
||||
status: 'completed',
|
||||
};
|
||||
}
|
||||
updateAssistantMessage(msgId, {
|
||||
reasoning_steps: [...currentSteps.value],
|
||||
});
|
||||
|
||||
if (props.enableNodeEvents) {
|
||||
emit('node-complete', {
|
||||
node_id: event.node_id,
|
||||
node_type: event.node_type,
|
||||
status: event.status,
|
||||
elapsed_time: event.elapsed_time,
|
||||
tokens_used: event.tokens_used,
|
||||
error_message: event.error_message,
|
||||
outputs: event.outputs,
|
||||
inputs: event.inputs,
|
||||
loop_iteration: event.loop_iteration,
|
||||
warnings: event.warnings,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'node_event': {
|
||||
const nodeEvent = event.event;
|
||||
if (nodeEvent?.type === 'message') {
|
||||
const initialMsg = messages.value.find((m) => m.id === msgId);
|
||||
if (initialMsg && !initialMsg.content?.trim()) {
|
||||
updateAssistantMessage(msgId, {
|
||||
status: 'completed',
|
||||
content: nodeEvent.content || '',
|
||||
});
|
||||
} else {
|
||||
const newMsg: ChatMessage = {
|
||||
id: generateId(),
|
||||
role: 'assistant',
|
||||
content: nodeEvent.content || '',
|
||||
timestamp: new Date(),
|
||||
status: 'completed',
|
||||
};
|
||||
messages.value.push(newMsg);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// ========== 节点事件 ==========
|
||||
case 'node_start': {
|
||||
const existingIndex = currentSteps.value.findIndex(
|
||||
(s) => s.node_id === event.node_id,
|
||||
);
|
||||
if (existingIndex === -1) {
|
||||
currentSteps.value.push({
|
||||
type: 'node_start',
|
||||
content: getNodeLabel(
|
||||
event.node_id,
|
||||
event.node_type,
|
||||
event.node_label,
|
||||
),
|
||||
node_id: event.node_id,
|
||||
node_type: event.node_type,
|
||||
status: 'running',
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
updateAssistantMessage(msgId, {
|
||||
reasoning_steps: [...currentSteps.value],
|
||||
});
|
||||
|
||||
if (props.enableNodeEvents) {
|
||||
emit('node-start', {
|
||||
node_id: event.node_id,
|
||||
node_type: event.node_type,
|
||||
node_label: event.node_label,
|
||||
inputs: event.inputs,
|
||||
loop_iteration: event.loop_iteration,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// ========== 通用事件 ==========
|
||||
case 'start': {
|
||||
currentRunId.value = event.run_id || '';
|
||||
break;
|
||||
}
|
||||
|
||||
// ========== 对话流交互事件 ==========
|
||||
case 'waiting_input': {
|
||||
waitingForInput.value = true;
|
||||
waitingConfig.value = event.waiting_config || event.config;
|
||||
|
||||
const configData = event.waiting_config || (event as any).config;
|
||||
|
||||
if (configData?.type === 'design_preview') {
|
||||
handleDesignPreview(event, msgId, configData);
|
||||
} else {
|
||||
handleDialogFlowInteraction(msgId, configData);
|
||||
}
|
||||
|
||||
running.value = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
handleStreamEvent,
|
||||
updateAssistantMessage,
|
||||
getWaitingPrompt,
|
||||
generateId,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user