Files
ai-agent-admin/web/apps/web-ele/src/components/ai-chat-panel/composables/useChatApi.ts
T

269 lines
8.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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<typeof ref<null | string>>;
} {
// 对话 IDAgent 模式使用)
const conversationId = ref<null | string>(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<string, any> | 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<string, any> | 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<ChatMessage[]> => {
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 };
}