Build lightweight AI agent admin

This commit is contained in:
Codex
2026-06-08 18:14:59 +08:00
commit e164840f43
2530 changed files with 435693 additions and 0 deletions
@@ -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,
};
}