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,268 @@
/**
* 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 };
}