/** * Chat API 适配器 * * 根据不同模式调用不同的 API */ import type { AiChatPanelProps, Attachment, ChatApiAdapter, ChatMessage, StreamEvent, } from '../types'; import { ref, toRef, watch } from 'vue'; import { feedbackAgentMessageApi, getAgentMessagesApi, resumeWorkflowStreamApi, runWorkflowStreamApi, sendAgentMessageStream, } from '#/api/ai-platform/ai-platform'; import { getFileUrl } from '#/composables/useFileUrl'; /** * 创建 Chat API 适配器 */ export function useChatApi(props: AiChatPanelProps): { adapter: ChatApiAdapter; conversationId: ReturnType>; } { // 对话 ID(Agent 模式使用) const conversationId = ref(props.conversationId || null); // 使用 toRef 确保响应式追踪 const propsConversationId = toRef(props, 'conversationId'); // 监听 props.conversationId 变化,同步到内部状态 watch(propsConversationId, (newId) => { // 同步 props 的 conversationId 到内部状态 conversationId.value = newId || null; }); // ========== 工作流模式适配器 ========== if (props.mode === 'workflow') { const adapter: ChatApiAdapter = { sendMessage: ( _message: string, inputs: Record | undefined, onEvent: (event: StreamEvent) => void, onError: (error: Error) => void, onComplete: () => void, ) => { if (!props.workflowId) { onError(new Error('工作流 ID 未配置')); return null; } return runWorkflowStreamApi( props.workflowId, inputs || {}, onEvent, onError, onComplete, props.useDraft ?? true, // 工作流模式默认使用草稿版本 ); }, resumeExecution: ( runId: string, userInput: any, onEvent: (event: StreamEvent) => void, onError: (error: Error) => void, onComplete: () => void, ) => { return resumeWorkflowStreamApi( runId, userInput, onEvent, onError, onComplete, ); }, }; return { adapter, conversationId }; } // ========== Agent 模式适配器 ========== const adapter: ChatApiAdapter = { sendMessage: ( message: string, inputs: Record | undefined, onEvent: (event: StreamEvent) => void, onError: (error: Error) => void, onComplete: () => void, attachments?: Attachment[], ) => { if (!props.agentId) { onError(new Error('Agent ID 未配置')); return null; } // 转换附件格式,使用 file_id(优先)或 id const attachmentInputs = attachments ?.filter((att) => att.file_id || att.id) .map((att) => ({ file_id: att.file_id || att.id!, type: att.type, })); // 从 inputs 中提取 form_code(如果存在) const formCode = inputs?.form_code; return sendAgentMessageStream( props.agentId, { message, conversation_id: conversationId.value || undefined, attachments: attachmentInputs, form_code: formCode, }, (event) => { // 捕获 conversation_id if (event.type === 'start' && event.conversation_id) { conversationId.value = event.conversation_id; } onEvent(event); }, onError, onComplete, ); }, resumeExecution: ( _runId: string, userInput: any, onEvent: (event: StreamEvent) => void, onError: (error: Error) => void, onComplete: () => void, ) => { if (!props.agentId) { onError(new Error('Agent ID 未配置')); return null; } // Agent 模式的恢复逻辑与发送相同 return sendAgentMessageStream( props.agentId, { message: typeof userInput === 'string' ? userInput : JSON.stringify(userInput), conversation_id: conversationId.value || undefined, }, onEvent, onError, onComplete, ); }, // Agent-chat 模式支持历史加载 loadHistory: props.mode === 'agent-chat' && props.enableHistory !== false ? async (convId: string): Promise => { const res = await getAgentMessagesApi(convId, { pageSize: 100 }); conversationId.value = convId; // 后端返回的是数组或分页对象,需要兼容处理 const items = Array.isArray(res) ? res : res.items || []; // 转换为 ChatMessage 格式 const messages = await Promise.all( items.map(async (msg: any) => { // 处理附件,为图片加载完整 URL let attachments: Attachment[] | undefined; if (msg.attachments?.length) { attachments = await Promise.all( msg.attachments.map(async (att: any) => { const fileId = att.file_id || att.id; const attType = att.type || 'file'; const mimeType = att.mime_type || ''; // 为图片附件加载完整 URL let url = att.url; let thumbnail = att.thumbnail; if ( (attType === 'image' || mimeType.startsWith('image/')) && fileId ) { try { const imageUrl = await getFileUrl(fileId); url = imageUrl || url; thumbnail = imageUrl || thumbnail; } catch (error) { console.warn( 'Failed to load image URL for attachment:', fileId, error, ); } } return { id: fileId, file_id: fileId, type: attType, name: att.name, url, size: att.size, thumbnail, mime_type: mimeType, } as Attachment; }), ); } return { id: msg.id, role: msg.role as 'assistant' | 'user', content: msg.content, status: msg.status as any, timestamp: msg.sys_create_datetime || msg.created_at, elapsed_time: msg.elapsed_time, tokens_used: msg.total_tokens, error_message: msg.error_message, feedback: msg.feedback || null, reasoning_steps: msg.reasoning_steps?.map((step: any) => ({ type: step.type, content: step.content || '', tool: step.tool, params: step.params, node_id: step.node_id, node_type: step.node_type, output: step.output, status: step.status, timestamp: step.timestamp, })), interaction: msg.interaction, voice: msg.voice, attachments, }; }), ); // 按时间升序排序 return messages.sort( (a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime(), ); } : undefined, // Agent-chat 模式支持反馈 sendFeedback: props.mode === 'agent-chat' && props.enableFeedback !== false ? async (messageId: string, feedback: 'dislike' | 'like') => { await feedbackAgentMessageApi(messageId, { feedback }); } : undefined, }; return { adapter, conversationId }; }