feat: 支持智能体文档附件与轻量读取

- 建立文档上传、异步读取、对象存储、补偿与聊天绑定闭环

- 按智能体 20K 上下文预算选择文档片段并保留稳定引用

- 统一聊天文件卡片、类型图标、草稿恢复与可靠下载
This commit is contained in:
2026-07-29 01:32:19 +08:00
parent fceedd02cd
commit d45c67a317
72 changed files with 5876 additions and 237 deletions

View File

@@ -1,6 +1,12 @@
<script setup lang="ts">
import type {
ChatDocumentAttachment,
ChatDocumentLoader,
ChatImageAttachment,
ChatImageLoader,
} from '@easyflow/common-ui';
import type { AiChatMessage, AiToolApprovalPayload } from './types';
import type { ChatImageAttachment, ChatImageLoader } from '@easyflow/common-ui';
import { Close } from '@element-plus/icons-vue';
import { ElButton } from 'element-plus';
@@ -12,28 +18,43 @@ withDefaults(
defineProps<{
approvalLoading?: boolean;
closable?: boolean;
documentLoader?: ChatDocumentLoader;
documents?: ChatDocumentAttachment[];
emptyText?: string;
loading?: boolean;
images?: ChatImageAttachment[];
imageEnabled?: boolean;
imageLoader?: ChatImageLoader;
modelValue?: string;
images?: ChatImageAttachment[];
loading?: boolean;
messages: AiChatMessage[];
modelValue?: string;
placeholder?: string;
subtitle?: string;
title: string;
}>(),
{ imageEnabled: true },
{
documentLoader: undefined,
documents: () => [],
emptyText: '',
imageEnabled: true,
imageLoader: undefined,
images: () => [],
modelValue: '',
placeholder: '',
subtitle: '',
},
);
const emit = defineEmits<{
addDocumentFiles: [files: File[]];
addFiles: [files: File[]];
approve: [payload: AiToolApprovalPayload];
close: [];
reject: [payload: AiToolApprovalPayload];
send: [text: string];
removeDocument: [item: ChatDocumentAttachment];
removeImage: [item: ChatImageAttachment];
retryDocument: [item: ChatDocumentAttachment];
retryImage: [item: ChatImageAttachment];
send: [text: string];
stop: [];
'update:modelValue': [value: string];
}>();
@@ -76,6 +97,8 @@ defineSlots<{
</slot>
<AiPromptInput
:model-value="modelValue"
:documents="documents"
:document-loader="documentLoader"
:images="images"
:image-enabled="imageEnabled"
:image-loader="imageLoader"
@@ -83,8 +106,11 @@ defineSlots<{
:placeholder="placeholder"
@send="emit('send', $event)"
@add-files="emit('addFiles', $event)"
@add-document-files="emit('addDocumentFiles', $event)"
@remove-document="emit('removeDocument', $event)"
@remove-image="emit('removeImage', $event)"
@retry-image="emit('retryImage', $event)"
@retry-document="emit('retryDocument', $event)"
@stop="emit('stop')"
@update:model-value="emit('update:modelValue', $event)"
/>

View File

@@ -1,27 +1,49 @@
<script setup lang="ts">
import type { ChatImageAttachment, ChatImageLoader } from '@easyflow/common-ui';
import type {
ChatDocumentAttachment,
ChatDocumentLoader,
ChatImageAttachment,
ChatImageLoader,
} from '@easyflow/common-ui';
import { computed, ref } from 'vue';
import { ChatImageAttachments } from '@easyflow/common-ui';
import {
ChatDocumentAttachments,
ChatImageAttachments,
} from '@easyflow/common-ui';
import { Paperclip, Promotion } from '@element-plus/icons-vue';
import { ElButton, ElInput, ElMessage } from 'element-plus';
const props = withDefaults(
defineProps<{
loading?: boolean;
images?: ChatImageAttachment[];
documentLoader?: ChatDocumentLoader;
documents?: ChatDocumentAttachment[];
imageEnabled?: boolean;
imageLoader?: ChatImageLoader;
images?: ChatImageAttachment[];
loading?: boolean;
modelValue?: string;
placeholder?: string;
}>(),
{ imageEnabled: true },
{
documentLoader: undefined,
documents: () => [],
imageEnabled: true,
imageLoader: undefined,
images: () => [],
modelValue: '',
placeholder: '',
},
);
const emit = defineEmits<{
addDocumentFiles: [files: File[]];
addFiles: [files: File[]];
removeDocument: [item: ChatDocumentAttachment];
removeImage: [item: ChatImageAttachment];
retryDocument: [item: ChatDocumentAttachment];
retryImage: [item: ChatImageAttachment];
send: [text: string];
stop: [];
@@ -40,34 +62,56 @@ const hasReadyImage = computed(() =>
const hasPendingImage = computed(() =>
(props.images || []).some((item) => item.status !== 'ready'),
);
const hasReadyDocument = computed(() =>
(props.documents || []).some((item) => item.status === 'ready'),
);
const hasPendingDocument = computed(() =>
(props.documents || []).some((item) => item.status !== 'ready'),
);
const canSend = computed(
() =>
(text.value.trim().length > 0 || hasReadyImage.value) &&
(text.value.trim().length > 0 ||
hasReadyImage.value ||
hasReadyDocument.value) &&
(!hasReadyImage.value || props.imageEnabled !== false) &&
!hasPendingImage.value &&
!hasPendingDocument.value &&
!props.loading,
);
const fileAccept = computed(() => {
const documents =
'.pdf,.doc,.docx,.ppt,.pptx,.xls,.xlsx,.txt,.md,application/pdf,text/plain,text/markdown';
if (props.imageEnabled === false) return documents;
return `${documents},.png,.jpg,.jpeg,.webp,.gif,.bmp,image/png,image/jpeg,image/webp,image/gif,image/bmp`;
});
function send() {
const value = text.value.trim();
if (
(!value && !hasReadyImage.value) ||
(!value && !hasReadyImage.value && !hasReadyDocument.value) ||
props.loading ||
hasPendingImage.value
hasPendingImage.value ||
hasPendingDocument.value
)
return;
emit('send', value);
}
function chooseFiles() {
if (props.imageEnabled === false) return;
fileInput.value?.click();
}
function handleFiles(event: Event) {
const target = event.target as HTMLInputElement;
const files = [...(target.files || [])];
if (files.length) emit('addFiles', files);
const imageFiles = files.filter((file) => file.type.startsWith('image/'));
const documentFiles = files.filter((file) => !file.type.startsWith('image/'));
if (imageFiles.length > 0 && props.imageEnabled !== false) {
emit('addFiles', imageFiles);
}
if (documentFiles.length > 0) {
emit('addDocumentFiles', documentFiles);
}
target.value = '';
}
@@ -76,7 +120,7 @@ function handlePaste(event: ClipboardEvent) {
const files = [...(event.clipboardData?.files || [])].filter((file) =>
file.type.startsWith('image/'),
);
if (!files.length) return;
if (files.length === 0) return;
event.preventDefault();
emit('addFiles', files);
ElMessage.success(
@@ -87,8 +131,7 @@ function handlePaste(event: ClipboardEvent) {
function handleDragEnter(event: DragEvent) {
if (
!props.loading &&
props.imageEnabled !== false &&
(props.images?.length || 0) < 5 &&
((props.images?.length || 0) < 5 || (props.documents?.length || 0) < 3) &&
[...(event.dataTransfer?.types || [])].includes('Files')
) {
dragActive.value = true;
@@ -109,14 +152,18 @@ function handleDrop(event: DragEvent) {
dragActive.value = false;
if (
props.loading ||
props.imageEnabled === false ||
(props.images?.length || 0) >= 5
((props.images?.length || 0) >= 5 && (props.documents?.length || 0) >= 3)
)
return;
const files = [...(event.dataTransfer?.files || [])].filter((file) =>
file.type.startsWith('image/'),
);
if (files.length) emit('addFiles', files);
const files = [...(event.dataTransfer?.files || [])];
const imageFiles = files.filter((file) => file.type.startsWith('image/'));
const documentFiles = files.filter((file) => !file.type.startsWith('image/'));
if (imageFiles.length > 0 && props.imageEnabled !== false) {
emit('addFiles', imageFiles);
}
if (documentFiles.length > 0) {
emit('addDocumentFiles', documentFiles);
}
}
function stop() {
@@ -151,6 +198,16 @@ function handleKeydown(event: Event | KeyboardEvent) {
@remove="emit('removeImage', $event)"
@retry="emit('retryImage', $event)"
/>
<ChatDocumentAttachments
v-if="documents?.length"
class="ai-prompt-input__documents"
:items="documents"
:document-loader="documentLoader"
removable
retryable
@remove="emit('removeDocument', $event)"
@retry="emit('retryDocument', $event)"
/>
<ElInput
v-model="text"
class="ai-prompt-input__textarea"
@@ -163,22 +220,22 @@ function handleKeydown(event: Event | KeyboardEvent) {
@keydown="handleKeydown"
/>
<input
v-if="imageEnabled !== false"
ref="fileInput"
class="ai-prompt-input__file"
type="file"
accept=".png,.jpg,.jpeg,.webp,.gif,.bmp,image/png,image/jpeg,image/webp,image/gif,image/bmp"
:accept="fileAccept"
multiple
@change="handleFiles"
/>
<ElButton
v-if="imageEnabled !== false"
:icon="Paperclip"
circle
text
:disabled="loading || (images?.length || 0) >= 5"
aria-label="添加图片"
title="添加图片"
:disabled="
loading || ((images?.length || 0) >= 5 && (documents?.length || 0) >= 3)
"
aria-label="添加附件"
title="添加附件"
class="ai-prompt-input__attach"
@click="chooseFiles"
/>
@@ -229,6 +286,10 @@ function handleKeydown(event: Event | KeyboardEvent) {
flex: 0 0 100%;
}
.ai-prompt-input__documents {
flex: 0 0 100%;
}
.ai-prompt-input__file {
display: none;
}

View File

@@ -0,0 +1,65 @@
import type { ChatDocumentAttachment } from '@easyflow/common-ui';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { loadAgentChatDocument } from './mediaApi';
const requestApi = vi.hoisted(() => ({
download: vi.fn(),
}));
vi.mock('#/api/request', () => ({
api: requestApi,
}));
const documentAttachment: ChatDocumentAttachment = {
downloadUrl:
'/api/v1/agent/media/document/content?reference=document:attachment',
name: '需求说明.docx',
status: 'ready',
};
describe('agent chat document download', () => {
beforeEach(() => {
vi.useFakeTimers();
vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:document');
vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => undefined);
vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(
() => undefined,
);
});
afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
requestApi.download.mockReset();
});
it('downloads a non-empty blob and revokes its URL after the browser takes over', async () => {
requestApi.download.mockResolvedValue(new Blob(['document']));
await loadAgentChatDocument(documentAttachment);
expect(requestApi.download).toHaveBeenCalledWith(
documentAttachment.downloadUrl,
);
expect(URL.createObjectURL).toHaveBeenCalledTimes(1);
expect(HTMLAnchorElement.prototype.click).toHaveBeenCalledTimes(1);
expect(URL.revokeObjectURL).not.toHaveBeenCalled();
await vi.advanceTimersByTimeAsync(1000);
expect(URL.revokeObjectURL).toHaveBeenCalledWith('blob:document');
});
it('rejects an empty response before creating a damaged download', async () => {
requestApi.download.mockResolvedValue(new Blob([]));
await expect(loadAgentChatDocument(documentAttachment)).rejects.toThrow(
'下载内容为空,请稍后重试',
);
expect(URL.createObjectURL).not.toHaveBeenCalled();
expect(HTMLAnchorElement.prototype.click).not.toHaveBeenCalled();
});
});

View File

@@ -1,7 +1,12 @@
import type { ChatImageAttachment } from '@easyflow/common-ui';
import type {
ChatDocumentAttachment,
ChatImageAttachment,
} from '@easyflow/common-ui';
import { api } from '#/api/request';
const DOCUMENT_URL_REVOKE_DELAY_MS = 1000;
export type AgentComposerMode = 'DRAFT' | 'FORMAL';
export interface AgentMediaUpload extends ChatImageAttachment {
@@ -15,8 +20,16 @@ export interface AgentMediaUpload extends ChatImageAttachment {
width: number;
}
export interface AgentDocumentUpload extends ChatDocumentAttachment {
expiresAt?: string;
status: 'error' | 'reading' | 'ready' | 'uploading';
uploadId: string;
}
export interface AgentComposerDraftPayload {
agentId: string;
documentUploadIds: string[];
documents?: AgentDocumentUpload[];
expiresAt?: string;
imageUploadIds: string[];
images?: AgentMediaUpload[];
@@ -63,6 +76,49 @@ export function deleteAgentChatImage(uploadId: string) {
});
}
export function uploadAgentChatDocument(
file: File,
context: {
agentId: string;
mode: AgentComposerMode;
sessionId: string;
},
uploadId?: string,
) {
const body = new FormData();
body.append('file', file);
body.append('mode', context.mode);
body.append('agentId', context.agentId);
body.append('sessionId', context.sessionId);
if (uploadId) {
body.append('uploadId', uploadId);
}
return api.postFile<RequestResult<AgentDocumentUpload>>(
'/api/v1/agent/media/document/upload',
body,
);
}
export function getAgentChatDocumentStatus(uploadId: string) {
return api.get<RequestResult<AgentDocumentUpload>>(
'/api/v1/agent/media/document/status',
{ params: { uploadId } },
);
}
export function retryAgentChatDocument(uploadId: string) {
return api.post<RequestResult<AgentDocumentUpload>>(
'/api/v1/agent/media/document/retry',
{ uploadId },
);
}
export function deleteAgentChatDocument(uploadId: string) {
return api.post<RequestResult<void>>('/api/v1/agent/media/document/delete', {
uploadId,
});
}
export function getAgentComposerDraft(params: {
agentId: string;
mode: AgentComposerMode;
@@ -84,6 +140,7 @@ export function saveAgentComposerDraft(data: AgentComposerDraftPayload) {
export function deleteAgentComposerDraft(data: {
agentId: string;
deleteUploads?: boolean;
documentUploadIds?: string[];
imageUploadIds?: string[];
mode: AgentComposerMode;
sessionId: string;
@@ -95,7 +152,28 @@ export function deleteAgentComposerDraft(data: {
}
export async function loadAgentChatImage(previewUrl: string) {
if (!previewUrl || /^(blob:|data:)/i.test(previewUrl)) return previewUrl;
if (!previewUrl || /^(?:blob:|data:)/i.test(previewUrl)) return previewUrl;
const blob = await api.download<Blob>(previewUrl);
return URL.createObjectURL(blob);
}
export async function loadAgentChatDocument(item: ChatDocumentAttachment) {
if (!item.downloadUrl) return;
const blob = await api.download<Blob>(item.downloadUrl);
if (!(blob instanceof Blob) || blob.size <= 0) {
throw new Error('下载内容为空,请稍后重试');
}
const url = URL.createObjectURL(blob);
const anchor = document.createElement('a');
anchor.href = url;
anchor.download = item.name || '文档';
try {
document.body.append(anchor);
anchor.click();
} finally {
anchor.remove();
window.setTimeout(() => {
URL.revokeObjectURL(url);
}, DOCUMENT_URL_REVOKE_DELAY_MS);
}
}

View File

@@ -1,10 +1,10 @@
// @vitest-environment happy-dom
import { useUserStore } from '@easyflow/stores';
import { createPinia, setActivePinia } from 'pinia';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { useUserStore } from '@easyflow/stores';
import {
allocateAgentComposerSession,
deleteAgentComposerDraft,
@@ -15,11 +15,15 @@ import { useAgentComposerDraft } from './useAgentComposerDraft';
vi.mock('./mediaApi', () => ({
allocateAgentComposerSession: vi.fn(),
deleteAgentChatDocument: vi.fn(),
deleteAgentChatImage: vi.fn(),
deleteAgentComposerDraft: vi.fn(),
getAgentChatDocumentStatus: vi.fn(),
getAgentComposerDraft: vi.fn(),
loadAgentChatImage: vi.fn(),
retryAgentChatDocument: vi.fn(),
saveAgentComposerDraft: vi.fn(),
uploadAgentChatDocument: vi.fn(),
uploadAgentChatImage: vi.fn(),
}));
@@ -174,6 +178,7 @@ describe('useAgentComposerDraft', () => {
expect(deleteAgentComposerDraft).toHaveBeenCalledWith({
agentId: 'agent-1',
deleteUploads: false,
documentUploadIds: [],
imageUploadIds: [],
mode: 'DRAFT',
sessionId: 'agent-draft-100',

View File

@@ -1,3 +1,5 @@
import type { AgentComposerDraftPayload, AgentComposerMode } from './mediaApi';
import { ref } from 'vue';
import { useUserStore } from '@easyflow/stores';
@@ -13,7 +15,7 @@ import {
getAgentComposerDraft,
saveAgentComposerDraft,
} from './mediaApi';
import type { AgentComposerDraftPayload, AgentComposerMode } from './mediaApi';
import { useChatDocumentUploads } from './useChatDocumentUploads';
import { useChatImageUploads } from './useChatImageUploads';
const SHADOW_TTL = 24 * 60 * 60 * 1000;
@@ -31,6 +33,7 @@ export function useAgentComposerDraft(mode: AgentComposerMode) {
const text = ref('');
const revision = ref(0);
const images = useChatImageUploads();
const documents = useChatDocumentUploads();
let saveTimer: ReturnType<typeof setTimeout> | undefined;
let activation = 0;
let changeSequence = 0;
@@ -140,6 +143,7 @@ export function useAgentComposerDraft(mode: AgentComposerMode) {
function payloadOf(): AgentComposerDraftPayload {
return {
agentId: agentId.value,
documentUploadIds: [...documents.uploadIds.value],
imageUploadIds: [...images.uploadIds.value],
mode,
revision: revision.value,
@@ -151,6 +155,7 @@ export function useAgentComposerDraft(mode: AgentComposerMode) {
function shadowPayload(): AgentComposerDraftPayload {
return {
...payloadOf(),
documents: documents.readyItems.value.map((item) => ({ ...item })),
images: images.readyItems.value.map((item) => ({ ...item })),
};
}
@@ -160,6 +165,7 @@ export function useAgentComposerDraft(mode: AgentComposerMode) {
text.value = draft.text || '';
revision.value = Number(draft.revision || 0);
images.restore(draft.images || []);
documents.restore(draft.documents || []);
changeSequence++;
}
@@ -212,7 +218,8 @@ export function useAgentComposerDraft(mode: AgentComposerMode) {
(Boolean(saveTimer) ||
pendingOperations > 0 ||
Boolean(text.value.trim()) ||
images.uploadIds.value.length > 0)
images.uploadIds.value.length > 0 ||
documents.uploadIds.value.length > 0)
) {
await flush();
}
@@ -226,6 +233,7 @@ export function useAgentComposerDraft(mode: AgentComposerMode) {
text.value = '';
revision.value = 0;
images.clear();
documents.clear();
if (!targetAgentId) return;
const shadow = readShadow(targetAgentId, preferredSessionId);
try {
@@ -276,7 +284,11 @@ export function useAgentComposerDraft(mode: AgentComposerMode) {
}
const request = shadowPayload();
const savedSequence = changeSequence;
if (!request.text.trim() && request.imageUploadIds.length === 0) {
if (
!request.text.trim() &&
request.imageUploadIds.length === 0 &&
request.documentUploadIds.length === 0
) {
clearShadow(request.agentId, request.sessionId, context.identity);
const response = await deleteAgentComposerDraft({
agentId: request.agentId,
@@ -335,6 +347,7 @@ export function useAgentComposerDraft(mode: AgentComposerMode) {
const scope = {
identity: identityScope(),
agentId: agentId.value,
documentUploadIds: [...documents.uploadIds.value],
imageUploadIds: [...images.uploadIds.value],
mode,
sessionId: sessionId.value,
@@ -345,11 +358,13 @@ export function useAgentComposerDraft(mode: AgentComposerMode) {
text.value = '';
revision.value = 0;
images.clear();
documents.clear();
if (deleteRemote && scope.agentId && scope.sessionId) {
await enqueue(async () => {
const response = await deleteAgentComposerDraft({
agentId: scope.agentId,
deleteUploads,
documentUploadIds: scope.documentUploadIds,
imageUploadIds: scope.imageUploadIds,
mode: scope.mode,
sessionId: scope.sessionId,
@@ -373,11 +388,13 @@ export function useAgentComposerDraft(mode: AgentComposerMode) {
(saveTimer ||
pendingOperations > 0 ||
text.value.trim() ||
images.uploadIds.value.length > 0)
images.uploadIds.value.length > 0 ||
documents.uploadIds.value.length > 0)
) {
await flush();
text.value = '';
images.clear();
documents.clear();
}
const response = await allocateAgentComposerSession(mode);
if (response.errorCode !== 0 || !response.data?.sessionId) {
@@ -390,6 +407,7 @@ export function useAgentComposerDraft(mode: AgentComposerMode) {
activate,
agentId,
clear,
documents,
ensureSession,
flush,
images,

View File

@@ -0,0 +1,98 @@
import { nextTick, watchEffect } from 'vue';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { useChatDocumentUploads } from './useChatDocumentUploads';
const mediaApi = vi.hoisted(() => ({
deleteAgentChatDocument: vi.fn(),
getAgentChatDocumentStatus: vi.fn(),
retryAgentChatDocument: vi.fn(),
uploadAgentChatDocument: vi.fn(),
}));
vi.mock('./mediaApi', () => mediaApi);
describe('useChatDocumentUploads', () => {
afterEach(() => {
vi.restoreAllMocks();
Object.values(mediaApi).forEach((mock) => mock.mockReset());
});
it('reactively exposes the ready state after upload', async () => {
let resolveUpload: (value: any) => void = () => undefined;
mediaApi.uploadAgentChatDocument.mockReturnValue(
new Promise((resolve) => {
resolveUpload = resolve;
}),
);
const uploads = useChatDocumentUploads();
const observedStatuses: (string | undefined)[] = [];
const stop = watchEffect(() => {
observedStatuses.push(uploads.items.value[0]?.status);
});
const pending = uploads.addFiles(
[
new File(['document'], 'test.docx', {
type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
}),
],
{ agentId: 'agent-1', mode: 'FORMAL', sessionId: 'session-1' },
);
await nextTick();
resolveUpload({
data: {
attachmentRef: 'document:test',
mimeType:
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
name: 'test.docx',
size: 8,
status: 'READY',
uploadId: 'upload-1',
},
errorCode: 0,
});
await pending;
await nextTick();
stop();
expect(observedStatuses).toContain('uploading');
expect(observedStatuses.at(-1)).toBe('ready');
expect(uploads.uploadIds.value).toEqual(['upload-1']);
expect(mediaApi.uploadAgentChatDocument).toHaveBeenCalledWith(
expect.any(File),
{ agentId: 'agent-1', mode: 'FORMAL', sessionId: 'session-1' },
expect.stringMatching(/^[a-f0-9]{32}$/),
);
});
it('exposes a server read failure without continuing to poll', async () => {
mediaApi.uploadAgentChatDocument.mockResolvedValue({
data: {
attachmentRef: 'document:failed',
errorMessage: '未检测到可读取文字',
mimeType: 'application/pdf',
name: 'scan.pdf',
size: 8,
status: 'READ_FAILED',
uploadId: 'upload-failed',
},
errorCode: 0,
});
const uploads = useChatDocumentUploads();
await uploads.addFiles(
[new File(['document'], 'scan.pdf', { type: 'application/pdf' })],
{ agentId: 'agent-1', mode: 'FORMAL', sessionId: 'session-1' },
);
expect(uploads.items.value[0]).toMatchObject({
error: '未检测到可读取文字',
status: 'error',
uploadId: 'upload-failed',
});
expect(mediaApi.getAgentChatDocumentStatus).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,20 @@
import { createChatDocumentUploads } from '@easyflow/common-ui';
import {
deleteAgentChatDocument,
getAgentChatDocumentStatus,
retryAgentChatDocument,
uploadAgentChatDocument,
} from './mediaApi';
/**
* 创建接入 Agent 文档接口的聊天附件状态组合。
*/
export function useChatDocumentUploads() {
return createChatDocumentUploads({
delete: deleteAgentChatDocument,
retry: retryAgentChatDocument,
status: getAgentChatDocumentStatus,
upload: uploadAgentChatDocument,
});
}