- 接入 AG-UI 运行投影、Turn 时间线和审批隔离 - 增加 Agent Skill 冻结绑定与运行时消费闭环 - 增加受控工作区、内置工具和私有 Artifact 生命周期
445 lines
12 KiB
Vue
445 lines
12 KiB
Vue
<script setup lang="ts">
|
|
import type {
|
|
ChatDocumentAttachment,
|
|
ChatImageAttachment,
|
|
ChatTimelineMessageItem,
|
|
ChatTimelineToolApprovalPayload,
|
|
} from '@easyflow/common-ui';
|
|
|
|
import type {
|
|
AgentInfo,
|
|
AgentKnowledgeBinding,
|
|
AgentSkillBinding,
|
|
AgentToolBinding,
|
|
} from '../types';
|
|
|
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
|
|
|
import {
|
|
CHAT_DOCUMENT_SUPPORTED_FORMATS,
|
|
ChatTimeline,
|
|
defaultAssistantAvatar,
|
|
} from '@easyflow/common-ui';
|
|
import { BrushCleaning } from '@easyflow/icons';
|
|
import { copyTextToClipboard } from '@easyflow/utils';
|
|
|
|
import { ElButton, ElMessage } from 'element-plus';
|
|
|
|
import AiChatPanel from '#/components/ai-chat/AiChatPanel.vue';
|
|
import {
|
|
createAgentArtifactLoader,
|
|
loadAgentChatDocument,
|
|
loadAgentChatImage,
|
|
} from '#/components/ai-chat/mediaApi';
|
|
import { useAgentComposerDraft } from '#/components/ai-chat/useAgentComposerDraft';
|
|
import { CHAT_IMAGE_SUPPORTED_FORMATS } from '#/components/ai-chat/useChatImageUploads';
|
|
|
|
import { approveAgentRun, rejectAgentRun } from '../api';
|
|
import { useAgentTryoutStream } from '../composables/useAgentTryoutStream';
|
|
import { resolveInteractionDisplay } from '../interaction-config';
|
|
import AgentWelcomeState from './AgentWelcomeState.vue';
|
|
|
|
const props = defineProps<{
|
|
agent: AgentInfo;
|
|
imageEnabled?: boolean;
|
|
knowledgeBindings: AgentKnowledgeBinding[];
|
|
skillBindings: AgentSkillBinding[];
|
|
toolBindings: AgentToolBinding[];
|
|
}>();
|
|
|
|
const emit = defineEmits<{ close: [] }>();
|
|
|
|
const {
|
|
loading,
|
|
clearDraftSession,
|
|
dispose,
|
|
markToolApproving,
|
|
markToolRejected,
|
|
copyMessageText,
|
|
selectVariant,
|
|
syncDraftContext,
|
|
timelineItems,
|
|
sendDraft,
|
|
stop,
|
|
} = useAgentTryoutStream();
|
|
const approvalLoading = ref(false);
|
|
const composer = useAgentComposerDraft('DRAFT');
|
|
const loadDraftAgentArtifact = createAgentArtifactLoader(() => ({
|
|
agentId: String(props.agent.id || ''),
|
|
mode: 'DRAFT',
|
|
runtimeSessionId: composer.sessionId.value,
|
|
}));
|
|
const interactionDisplay = computed(() =>
|
|
resolveInteractionDisplay(props.agent),
|
|
);
|
|
|
|
function getDraftContext() {
|
|
return {
|
|
agent: props.agent,
|
|
skillBindings: props.skillBindings,
|
|
toolBindings: props.toolBindings,
|
|
knowledgeBindings: props.knowledgeBindings,
|
|
};
|
|
}
|
|
|
|
function syncCurrentDraftContext(restore = false) {
|
|
syncDraftContext(getDraftContext(), restore, composer.sessionId.value);
|
|
}
|
|
|
|
async function activateComposer() {
|
|
const agentId = String(props.agent.id || '');
|
|
if (!agentId) return;
|
|
try {
|
|
await composer.activate(agentId, `agent-draft-${agentId}`);
|
|
syncCurrentDraftContext(true);
|
|
} catch (error) {
|
|
ElMessage.warning(
|
|
error instanceof Error ? error.message : '输入草稿恢复失败',
|
|
);
|
|
}
|
|
}
|
|
|
|
onMounted(() => void activateComposer());
|
|
onBeforeUnmount(() => {
|
|
dispose();
|
|
});
|
|
|
|
watch(
|
|
() => [props.agent.id, props.agent.localId],
|
|
() => void activateComposer(),
|
|
);
|
|
|
|
watch(
|
|
() => [
|
|
props.agent,
|
|
props.knowledgeBindings,
|
|
props.skillBindings,
|
|
props.toolBindings,
|
|
],
|
|
() => {
|
|
syncCurrentDraftContext();
|
|
},
|
|
{ deep: true },
|
|
);
|
|
|
|
async function handleSend(prompt: string) {
|
|
if (loading.value || approvalLoading.value) return;
|
|
if (composer.images.uploading.value) {
|
|
ElMessage.warning('图片上传完成后再发送');
|
|
return;
|
|
}
|
|
if (composer.documents.processing.value) {
|
|
ElMessage.warning('文档读取完成后再发送');
|
|
return;
|
|
}
|
|
const failedImage = composer.images.items.value.find(
|
|
(item) => item.status === 'error',
|
|
);
|
|
if (failedImage) {
|
|
ElMessage.error(failedImage.error || '请处理上传失败的图片');
|
|
return;
|
|
}
|
|
const failedDocument = composer.documents.items.value.find(
|
|
(item) => item.status === 'error',
|
|
);
|
|
if (failedDocument) {
|
|
ElMessage.error(failedDocument.error || '请处理读取失败的文档');
|
|
return;
|
|
}
|
|
await composer.flush();
|
|
await sendDraft({
|
|
...getDraftContext(),
|
|
documentUploadIds: composer.documents.uploadIds.value,
|
|
documents: composer.documents.readyItems.value.map((item) => ({
|
|
...item,
|
|
})),
|
|
prompt,
|
|
imageUploadIds: composer.images.uploadIds.value,
|
|
images: composer.images.readyItems.value.map((item) => ({ ...item })),
|
|
sessionId: composer.sessionId.value,
|
|
onAccepted: () =>
|
|
composer.markAccepted().catch(() => {
|
|
ElMessage.warning('消息已发送,草稿将在过期后自动清理');
|
|
}),
|
|
});
|
|
}
|
|
|
|
function handleDraftTextInput() {
|
|
composer.scheduleSave();
|
|
}
|
|
|
|
function handleSuggestedQuestion(question: string) {
|
|
void handleSend(question);
|
|
}
|
|
|
|
function canCopyMessage(item: ChatTimelineMessageItem) {
|
|
if (item.role === 'user') {
|
|
return Boolean(copyMessageText(item).trim());
|
|
}
|
|
return Boolean(
|
|
item.roundId && item.roundCompleted && copyMessageText(item).trim(),
|
|
);
|
|
}
|
|
|
|
function canRegenerateMessage(item: ChatTimelineMessageItem) {
|
|
return (
|
|
item.role === 'assistant' &&
|
|
Boolean(item.roundId) &&
|
|
Boolean(item.roundCompleted)
|
|
);
|
|
}
|
|
|
|
async function handleCopyMessage(item: ChatTimelineMessageItem) {
|
|
const text = copyMessageText(item).trim();
|
|
if (!text) {
|
|
return false;
|
|
}
|
|
try {
|
|
await copyTextToClipboard(text);
|
|
return true;
|
|
} catch (error) {
|
|
console.error('复制消息失败:', error);
|
|
ElMessage.error('复制失败');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function handleRegenerateMessage(item: ChatTimelineMessageItem) {
|
|
void item;
|
|
}
|
|
|
|
function handleSelectPreviousVariant(item: ChatTimelineMessageItem) {
|
|
selectVariant(item, 'previous');
|
|
}
|
|
|
|
function handleSelectNextVariant(item: ChatTimelineMessageItem) {
|
|
selectVariant(item, 'next');
|
|
}
|
|
|
|
async function handleClearSession() {
|
|
try {
|
|
await clearDraftSession();
|
|
await composer.clear();
|
|
await activateComposer();
|
|
ElMessage.success('已清理会话');
|
|
} catch (error) {
|
|
ElMessage.error(error instanceof Error ? error.message : '清理会话失败');
|
|
}
|
|
}
|
|
|
|
async function handleAddImageFiles(files: File[]) {
|
|
if (!props.agent.id) {
|
|
ElMessage.warning('请先保存智能体');
|
|
return;
|
|
}
|
|
await composer.ensureSession();
|
|
const { rejectedCount, unsupportedCount } = await composer.images.addFiles(
|
|
files,
|
|
{
|
|
agentId: String(props.agent.id),
|
|
mode: 'DRAFT',
|
|
sessionId: composer.sessionId.value,
|
|
},
|
|
);
|
|
if (unsupportedCount > 0) {
|
|
ElMessage.warning({
|
|
grouping: true,
|
|
message: `不支持该图片格式,支持:${CHAT_IMAGE_SUPPORTED_FORMATS}`,
|
|
});
|
|
}
|
|
if (rejectedCount > 0) {
|
|
ElMessage.warning('每次最多添加 5 张图片');
|
|
}
|
|
composer.scheduleSave();
|
|
}
|
|
|
|
async function handleAddDocumentFiles(files: File[]) {
|
|
if (!props.agent.id) {
|
|
ElMessage.warning('请先保存智能体');
|
|
return;
|
|
}
|
|
await composer.ensureSession();
|
|
const { rejectedCount, unsupportedCount } = await composer.documents.addFiles(
|
|
files,
|
|
{
|
|
agentId: String(props.agent.id),
|
|
mode: 'DRAFT',
|
|
sessionId: composer.sessionId.value,
|
|
},
|
|
);
|
|
if (unsupportedCount > 0) {
|
|
ElMessage.warning({
|
|
grouping: true,
|
|
message: `不支持该文件格式,支持:${CHAT_DOCUMENT_SUPPORTED_FORMATS}`,
|
|
});
|
|
}
|
|
if (rejectedCount > 0) {
|
|
ElMessage.warning('每轮最多添加 3 份文档');
|
|
}
|
|
composer.scheduleSave();
|
|
}
|
|
|
|
async function handleRetryImage(item: ChatImageAttachment) {
|
|
await composer.images.retry(item, {
|
|
agentId: String(props.agent.id),
|
|
mode: 'DRAFT',
|
|
sessionId: composer.sessionId.value,
|
|
});
|
|
composer.scheduleSave();
|
|
}
|
|
|
|
async function handleRemoveImage(item: ChatImageAttachment) {
|
|
try {
|
|
await composer.images.remove(item);
|
|
composer.scheduleSave();
|
|
} catch (error) {
|
|
ElMessage.error(error instanceof Error ? error.message : '图片删除失败');
|
|
}
|
|
}
|
|
|
|
async function handleRetryDocument(item: ChatDocumentAttachment) {
|
|
await composer.documents.retry(item, {
|
|
agentId: String(props.agent.id),
|
|
mode: 'DRAFT',
|
|
sessionId: composer.sessionId.value,
|
|
});
|
|
composer.scheduleSave();
|
|
}
|
|
|
|
async function handleRemoveDocument(item: ChatDocumentAttachment) {
|
|
try {
|
|
await composer.documents.remove(item);
|
|
composer.scheduleSave();
|
|
} catch (error) {
|
|
ElMessage.error(error instanceof Error ? error.message : '文档删除失败');
|
|
}
|
|
}
|
|
|
|
function handleStop() {
|
|
if (!loading.value) {
|
|
return;
|
|
}
|
|
stop();
|
|
}
|
|
|
|
async function handleApprove(payload: ChatTimelineToolApprovalPayload) {
|
|
approvalLoading.value = true;
|
|
markToolApproving(payload);
|
|
try {
|
|
const res = await approveAgentRun(payload.approvalId);
|
|
if (res.errorCode === 0) {
|
|
ElMessage.success('已批准');
|
|
}
|
|
} catch (error) {
|
|
markToolRejected({
|
|
...payload,
|
|
reason: error instanceof Error ? error.message : '批准失败',
|
|
});
|
|
throw error;
|
|
} finally {
|
|
approvalLoading.value = false;
|
|
}
|
|
}
|
|
|
|
async function handleReject(payload: ChatTimelineToolApprovalPayload) {
|
|
approvalLoading.value = true;
|
|
markToolRejected({
|
|
...payload,
|
|
reason: '用户拒绝执行',
|
|
});
|
|
try {
|
|
await rejectAgentRun(payload.approvalId, '用户拒绝执行');
|
|
} finally {
|
|
approvalLoading.value = false;
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<AiChatPanel
|
|
v-model="composer.text.value"
|
|
:title="agent.name || '草稿试运行'"
|
|
empty-text="输入问题试运行当前智能体"
|
|
closable
|
|
:messages="[]"
|
|
:loading="loading"
|
|
:documents="composer.documents.items.value"
|
|
:document-loader="loadAgentChatDocument"
|
|
:images="composer.images.items.value"
|
|
:image-enabled="imageEnabled"
|
|
:image-loader="loadAgentChatImage"
|
|
:placeholder="interactionDisplay.inputPlaceholder"
|
|
:approval-loading="approvalLoading"
|
|
@send="handleSend"
|
|
@update:model-value="handleDraftTextInput"
|
|
@add-files="handleAddImageFiles"
|
|
@add-document-files="handleAddDocumentFiles"
|
|
@remove-document="handleRemoveDocument"
|
|
@remove-image="handleRemoveImage"
|
|
@retry-document="handleRetryDocument"
|
|
@retry-image="handleRetryImage"
|
|
@stop="handleStop"
|
|
@approve="handleApprove"
|
|
@reject="handleReject"
|
|
@close="emit('close')"
|
|
>
|
|
<template #headerActions>
|
|
<ElButton
|
|
:icon="BrushCleaning"
|
|
circle
|
|
text
|
|
:disabled="approvalLoading"
|
|
aria-label="清理会话"
|
|
title="清理会话"
|
|
@click="handleClearSession"
|
|
/>
|
|
</template>
|
|
<div class="agent-tryout__conversation">
|
|
<AgentWelcomeState
|
|
v-if="timelineItems.length === 0"
|
|
:agent-name="agent.name || '智能体'"
|
|
:avatar="agent.avatar"
|
|
:disabled="loading || approvalLoading"
|
|
:suggested-questions="interactionDisplay.suggestedQuestions"
|
|
:welcome-message="interactionDisplay.welcomeMessage"
|
|
@select-question="handleSuggestedQuestion"
|
|
/>
|
|
<ChatTimeline
|
|
v-else
|
|
:assistant-avatar="agent.avatar || defaultAssistantAvatar"
|
|
:artifact-loader="loadDraftAgentArtifact"
|
|
:items="timelineItems"
|
|
:document-loader="loadAgentChatDocument"
|
|
:image-loader="loadAgentChatImage"
|
|
empty-text="输入问题试运行当前智能体"
|
|
:approval-loading="approvalLoading"
|
|
:copy-action="handleCopyMessage"
|
|
:copyable="canCopyMessage"
|
|
:regenerable="canRegenerateMessage"
|
|
:regenerate-disabled="true"
|
|
@approve="handleApprove"
|
|
@regenerate-message="handleRegenerateMessage"
|
|
@reject="handleReject"
|
|
@select-next-variant="handleSelectNextVariant"
|
|
@select-previous-variant="handleSelectPreviousVariant"
|
|
/>
|
|
</div>
|
|
</AiChatPanel>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.agent-tryout__conversation {
|
|
display: flex;
|
|
flex: 1;
|
|
min-height: 0;
|
|
padding: 0 var(--space-4);
|
|
overflow: auto;
|
|
overscroll-behavior: contain;
|
|
}
|
|
|
|
.agent-tryout__conversation :deep(.chat-timeline) {
|
|
width: 100%;
|
|
min-height: 0;
|
|
}
|
|
</style>
|