269 lines
8.1 KiB
TypeScript
269 lines
8.1 KiB
TypeScript
/**
|
||
* 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>>;
|
||
} {
|
||
// 对话 ID(Agent 模式使用)
|
||
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 };
|
||
}
|