fix: 支持模型异常后继续文档对话
- 将本轮文档上下文纳入可持久化的 Agent 用户消息 =- 在异常提示中提供请重试动作并自动发送继续
This commit is contained in:
@@ -181,6 +181,57 @@ describe('agentChatRuntimeManager', () => {
|
||||
expect(agentChatRuntimeManager.getSnapshot('101')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('输入已确认后将模型错误转换为可重试的用户提示', async () => {
|
||||
let resolveRun: (() => void) | undefined;
|
||||
let runOptions: EasyFlowAguiRunOptions | undefined;
|
||||
aguiMocks.run.mockImplementation((options: EasyFlowAguiRunOptions) => {
|
||||
runOptions = options;
|
||||
return new Promise<void>((resolve) => {
|
||||
resolveRun = resolve;
|
||||
});
|
||||
});
|
||||
useUserStore().setUserInfo({
|
||||
avatar: '',
|
||||
id: 'retry-user',
|
||||
loginName: 'retry-user',
|
||||
nickname: '重试用户',
|
||||
tenantId: 'tenant-1',
|
||||
});
|
||||
|
||||
await agentChatRuntimeManager.start({
|
||||
agentId: 'agent-1',
|
||||
prompt: '介绍文档',
|
||||
sessionId: 'retry-session',
|
||||
});
|
||||
runOptions?.onEvent({
|
||||
name: easyFlowAguiCustomEvent.inputAccepted,
|
||||
type: EventType.CUSTOM,
|
||||
value: {},
|
||||
});
|
||||
runOptions?.onEvent({
|
||||
code: 'MODEL_ERROR',
|
||||
message: 'Retries exhausted: 2/2',
|
||||
runId: 'run-test',
|
||||
threadId: 'retry-session',
|
||||
type: EventType.RUN_ERROR,
|
||||
});
|
||||
resolveRun?.();
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
|
||||
const snapshot = agentChatRuntimeManager.getSnapshot('retry-session');
|
||||
expect(snapshot).toEqual(
|
||||
expect.objectContaining({
|
||||
error: '模型连接异常',
|
||||
retryContextReady: true,
|
||||
terminalOutcome: 'failed',
|
||||
}),
|
||||
);
|
||||
expect(snapshot?.items.find((item) => item.type === 'error')).toEqual(
|
||||
expect.objectContaining({ message: '模型连接异常' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('刷新恢复已确认输入时重新触发草稿清理回调', async () => {
|
||||
const account = {
|
||||
avatar: '',
|
||||
|
||||
@@ -45,6 +45,7 @@ interface RuntimeSessionState {
|
||||
prompt: string;
|
||||
projectionToolArgs: Record<string, string>;
|
||||
projectionToolNames: Record<string, string>;
|
||||
retryContextReady: boolean;
|
||||
runId?: string;
|
||||
roundId: string;
|
||||
sending: boolean;
|
||||
@@ -65,6 +66,7 @@ interface StoredRuntimeSession {
|
||||
prompt: string;
|
||||
projectionToolArgs?: Record<string, string>;
|
||||
projectionToolNames?: Record<string, string>;
|
||||
retryContextReady?: boolean;
|
||||
runId?: string;
|
||||
roundId: string;
|
||||
sending: boolean;
|
||||
@@ -86,10 +88,12 @@ interface StartOptions {
|
||||
images?: ChatImageAttachment[];
|
||||
onInputAccepted?: () => Promise<void> | void;
|
||||
prompt: string;
|
||||
retryContextReady?: boolean;
|
||||
sessionId?: string;
|
||||
}
|
||||
|
||||
const STORAGE_VERSION = 5;
|
||||
const STORAGE_VERSION = 6;
|
||||
const MODEL_CONNECTION_ERROR_MESSAGE = '模型连接异常';
|
||||
const STREAM_NOTIFY_INTERVAL_MS = 50;
|
||||
const STREAM_PERSIST_INTERVAL_MS = 300;
|
||||
const MAX_RUNTIME_SESSIONS_PER_IDENTITY = 10;
|
||||
@@ -177,6 +181,7 @@ function persistSession(state: RuntimeSessionState) {
|
||||
prompt: state.prompt,
|
||||
projectionToolArgs: state.projectionToolArgs,
|
||||
projectionToolNames: state.projectionToolNames,
|
||||
retryContextReady: state.retryContextReady,
|
||||
runId: state.runId,
|
||||
roundId: state.roundId,
|
||||
sending: state.sending,
|
||||
@@ -298,7 +303,7 @@ function restoreSession(identity: string, sessionId: string) {
|
||||
}
|
||||
const parsed = JSON.parse(raw) as StoredRuntimeSession;
|
||||
if (
|
||||
![3, 4, STORAGE_VERSION].includes(parsed.version) ||
|
||||
![3, 4, 5, STORAGE_VERSION].includes(parsed.version) ||
|
||||
parsed.sessionId !== sessionId
|
||||
) {
|
||||
return undefined;
|
||||
@@ -324,6 +329,7 @@ function restoreSession(identity: string, sessionId: string) {
|
||||
typeof parsed.projectionToolNames === 'object'
|
||||
? parsed.projectionToolNames
|
||||
: {},
|
||||
retryContextReady: Boolean(parsed.retryContextReady),
|
||||
runId: parsed.runId,
|
||||
roundId: parsed.roundId,
|
||||
sending: Boolean(parsed.sending && parsed.runId),
|
||||
@@ -386,6 +392,7 @@ function acceptInput(
|
||||
) {
|
||||
replaceAcceptedAttachments(state.items, state.roundId, payload);
|
||||
state.inputAccepted = true;
|
||||
state.retryContextReady = true;
|
||||
persistSession(state);
|
||||
notifyInputAccepted(state);
|
||||
}
|
||||
@@ -563,7 +570,9 @@ function finishRuntimeSuccess(state: RuntimeSessionState) {
|
||||
}
|
||||
|
||||
function finishRuntimeFailure(state: RuntimeSessionState, error: unknown) {
|
||||
state.error = errorMessage(error);
|
||||
state.error = state.retryContextReady
|
||||
? MODEL_CONNECTION_ERROR_MESSAGE
|
||||
: errorMessage(error);
|
||||
state.sending = false;
|
||||
state.completed = true;
|
||||
const last = state.items[state.items.length - 1];
|
||||
@@ -664,7 +673,9 @@ export const agentChatRuntimeManager = {
|
||||
event.type === EventType.RUN_ERROR &&
|
||||
event.code !== 'RUN_CANCELLED'
|
||||
) {
|
||||
current.error = event.message || '发送失败,请稍后再试';
|
||||
current.error = current.retryContextReady
|
||||
? MODEL_CONNECTION_ERROR_MESSAGE
|
||||
: event.message || '发送失败,请稍后再试';
|
||||
}
|
||||
observeTerminalEvent(current, event.type);
|
||||
applyAguiEventToTimeline(
|
||||
@@ -674,6 +685,9 @@ export const agentChatRuntimeManager = {
|
||||
onInputAccepted(payload) {
|
||||
acceptInput(current, payload);
|
||||
},
|
||||
runErrorMessage: current.retryContextReady
|
||||
? MODEL_CONNECTION_ERROR_MESSAGE
|
||||
: undefined,
|
||||
roundId: current.roundId,
|
||||
startedAt: current.startedAt,
|
||||
},
|
||||
@@ -757,6 +771,7 @@ export const agentChatRuntimeManager = {
|
||||
prompt: options.prompt,
|
||||
projectionToolArgs: {},
|
||||
projectionToolNames: {},
|
||||
retryContextReady: Boolean(options.retryContextReady),
|
||||
runId,
|
||||
roundId,
|
||||
sending: true,
|
||||
@@ -794,7 +809,9 @@ export const agentChatRuntimeManager = {
|
||||
event.type === EventType.RUN_ERROR &&
|
||||
event.code !== 'RUN_CANCELLED'
|
||||
) {
|
||||
current.error = event.message || '发送失败,请稍后再试';
|
||||
current.error = current.retryContextReady
|
||||
? MODEL_CONNECTION_ERROR_MESSAGE
|
||||
: event.message || '发送失败,请稍后再试';
|
||||
}
|
||||
observeTerminalEvent(current, event.type);
|
||||
applyAguiEventToTimeline(
|
||||
@@ -804,6 +821,9 @@ export const agentChatRuntimeManager = {
|
||||
onInputAccepted(payload) {
|
||||
acceptInput(current, payload);
|
||||
},
|
||||
runErrorMessage: current.retryContextReady
|
||||
? MODEL_CONNECTION_ERROR_MESSAGE
|
||||
: undefined,
|
||||
roundId,
|
||||
startedAt,
|
||||
},
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import type {
|
||||
ChatDocumentAttachment,
|
||||
ChatImageAttachment,
|
||||
ChatTimelineErrorItem,
|
||||
ChatTimelineItem,
|
||||
ChatTimelineMessageItem,
|
||||
ChatTimelineToolApprovalPayload,
|
||||
@@ -108,6 +109,7 @@ const loadingKnowledges = ref(false);
|
||||
const savingExtraKnowledges = ref(false);
|
||||
const sending = ref(false);
|
||||
const runtimeRunning = ref(false);
|
||||
const retryableErrorRoundId = ref('');
|
||||
const approvalLoadingKey = ref('');
|
||||
const knowledgeOptions = ref<{ label: string; value: string }[]>([]);
|
||||
const knowledgeMap = ref(new Map<string, { id: string; title: string }>());
|
||||
@@ -463,12 +465,19 @@ function syncRuntimeSnapshot(sessionId = currentSessionId.value) {
|
||||
: undefined;
|
||||
if (!snapshot) {
|
||||
sending.value = false;
|
||||
retryableErrorRoundId.value = '';
|
||||
return false;
|
||||
}
|
||||
currentSessionId.value = snapshot.sessionId;
|
||||
selectedAgentId.value = String(snapshot.agentId);
|
||||
timelineItems.value = snapshot.items;
|
||||
sending.value = snapshot.sending;
|
||||
retryableErrorRoundId.value =
|
||||
snapshot.terminalOutcome === 'failed' &&
|
||||
snapshot.retryContextReady &&
|
||||
!snapshot.sending
|
||||
? snapshot.roundId
|
||||
: '';
|
||||
maybeRefreshCompletedRuntimeSession(snapshot);
|
||||
if (snapshot.prompt && !currentSession.value) {
|
||||
upsertSessionRecord(
|
||||
@@ -492,15 +501,20 @@ async function loadConversation(sessionId: string) {
|
||||
if (!sessionId) {
|
||||
timelineItems.value = [];
|
||||
sending.value = false;
|
||||
retryableErrorRoundId.value = '';
|
||||
return;
|
||||
}
|
||||
const runtimeSnapshot = agentChatRuntimeManager.getSnapshot(sessionId);
|
||||
if (runtimeSnapshot?.sending) {
|
||||
if (
|
||||
runtimeSnapshot?.sending ||
|
||||
runtimeSnapshot?.terminalOutcome === 'failed'
|
||||
) {
|
||||
syncRuntimeSnapshot(sessionId);
|
||||
await syncSessionRoute(sessionId);
|
||||
return;
|
||||
}
|
||||
loadingConversation.value = true;
|
||||
retryableErrorRoundId.value = '';
|
||||
try {
|
||||
const detailRes = await getAgentSession(sessionId);
|
||||
const res = await getAgentConversation(sessionId);
|
||||
@@ -660,12 +674,25 @@ function buildCapabilities() {
|
||||
];
|
||||
}
|
||||
|
||||
async function sendContent(rawContent: string) {
|
||||
interface SendContentOptions {
|
||||
includeComposer?: boolean;
|
||||
retryContextReady?: boolean;
|
||||
}
|
||||
|
||||
async function sendContent(
|
||||
rawContent: string,
|
||||
options: SendContentOptions = {},
|
||||
) {
|
||||
const content = rawContent.trim();
|
||||
const includeComposer = options.includeComposer !== false;
|
||||
const readyImageCount = includeComposer
|
||||
? composer.images.readyItems.value.length
|
||||
: 0;
|
||||
const readyDocumentCount = includeComposer
|
||||
? composer.documents.readyItems.value.length
|
||||
: 0;
|
||||
if (
|
||||
(!content &&
|
||||
composer.images.readyItems.value.length === 0 &&
|
||||
composer.documents.readyItems.value.length === 0) ||
|
||||
(!content && readyImageCount === 0 && readyDocumentCount === 0) ||
|
||||
!selectedAgentId.value ||
|
||||
sending.value
|
||||
) {
|
||||
@@ -675,44 +702,54 @@ async function sendContent(rawContent: string) {
|
||||
ElMessage.warning('当前回复完成后再发送新消息');
|
||||
return;
|
||||
}
|
||||
if (composer.images.uploading.value) {
|
||||
if (includeComposer && composer.images.uploading.value) {
|
||||
ElMessage.warning('图片上传完成后再发送');
|
||||
return;
|
||||
}
|
||||
if (composer.documents.processing.value) {
|
||||
if (includeComposer && composer.documents.processing.value) {
|
||||
ElMessage.warning('文档读取完成后再发送');
|
||||
return;
|
||||
}
|
||||
const failedImage = composer.images.items.value.find(
|
||||
(item) => item.status === 'error',
|
||||
);
|
||||
const failedImage = includeComposer
|
||||
? composer.images.items.value.find((item) => item.status === 'error')
|
||||
: undefined;
|
||||
if (failedImage) {
|
||||
ElMessage.error(failedImage.error || '请处理上传失败的图片');
|
||||
return;
|
||||
}
|
||||
const failedDocument = composer.documents.items.value.find(
|
||||
(item) => item.status === 'error',
|
||||
);
|
||||
const failedDocument = includeComposer
|
||||
? composer.documents.items.value.find((item) => item.status === 'error')
|
||||
: undefined;
|
||||
if (failedDocument) {
|
||||
ElMessage.error(failedDocument.error || '请处理读取失败的文档');
|
||||
return;
|
||||
}
|
||||
await composer.flush();
|
||||
if (includeComposer) {
|
||||
await composer.flush();
|
||||
}
|
||||
sending.value = true;
|
||||
retryableErrorRoundId.value = '';
|
||||
try {
|
||||
const sessionId = await agentChatRuntimeManager.start({
|
||||
agentId: selectedAgentId.value,
|
||||
agentName: selectedAgent.value?.name,
|
||||
baseItems: timelineItems.value,
|
||||
capabilities: buildCapabilities(),
|
||||
documentUploadIds: composer.documents.uploadIds.value,
|
||||
documents: composer.documents.readyItems.value.map((item) => ({
|
||||
...item,
|
||||
})),
|
||||
imageUploadIds: composer.images.uploadIds.value,
|
||||
images: composer.images.readyItems.value.map((item) => ({ ...item })),
|
||||
onInputAccepted: markComposerInputAccepted,
|
||||
documentUploadIds: includeComposer
|
||||
? composer.documents.uploadIds.value
|
||||
: undefined,
|
||||
documents: includeComposer
|
||||
? composer.documents.readyItems.value.map((item) => ({ ...item }))
|
||||
: undefined,
|
||||
imageUploadIds: includeComposer
|
||||
? composer.images.uploadIds.value
|
||||
: undefined,
|
||||
images: includeComposer
|
||||
? composer.images.readyItems.value.map((item) => ({ ...item }))
|
||||
: undefined,
|
||||
onInputAccepted: includeComposer ? markComposerInputAccepted : undefined,
|
||||
prompt: content,
|
||||
retryContextReady: Boolean(options.retryContextReady),
|
||||
sessionId: composer.sessionId.value,
|
||||
});
|
||||
await bindCreatedSession(sessionId, content);
|
||||
@@ -725,6 +762,24 @@ async function sendContent(rawContent: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function modelErrorAction(item: ChatTimelineErrorItem) {
|
||||
return item.roundId === retryableErrorRoundId.value ? '请重试' : undefined;
|
||||
}
|
||||
|
||||
async function handleModelErrorRetry(item: ChatTimelineErrorItem) {
|
||||
if (
|
||||
item.roundId !== retryableErrorRoundId.value ||
|
||||
sending.value ||
|
||||
runtimeRunning.value
|
||||
) {
|
||||
return;
|
||||
}
|
||||
await sendContent('继续', {
|
||||
includeComposer: false,
|
||||
retryContextReady: true,
|
||||
});
|
||||
}
|
||||
|
||||
async function handleSend() {
|
||||
await sendContent(promptText.value);
|
||||
}
|
||||
@@ -1266,12 +1321,15 @@ onBeforeUnmount(() => {
|
||||
:artifact-loader="loadCurrentAgentArtifact"
|
||||
:items="timelineItems"
|
||||
:document-loader="loadAgentChatDocument"
|
||||
:error-action="modelErrorAction"
|
||||
:error-action-disabled="sending || runtimeRunning"
|
||||
:image-loader="loadAgentChatImage"
|
||||
empty-text="选择智能体后开始对话"
|
||||
:approval-loading="Boolean(approvalLoadingKey)"
|
||||
:copy-action="handleCopyMessage"
|
||||
:copyable="canCopyMessage"
|
||||
@approve="handleApprove"
|
||||
@error-action="handleModelErrorRetry"
|
||||
@reject="handleReject"
|
||||
@select-next-variant="() => undefined"
|
||||
@select-previous-variant="() => undefined"
|
||||
|
||||
@@ -18,6 +18,7 @@ import { easyFlowAguiCustomEvent } from './custom-events';
|
||||
export interface AguiTimelineProjectionOptions {
|
||||
finishedAt?: number;
|
||||
onInputAccepted?: (payload: Record<string, unknown>) => Promise<void> | void;
|
||||
runErrorMessage?: string;
|
||||
roundId?: string;
|
||||
startedAt?: number;
|
||||
}
|
||||
@@ -361,7 +362,7 @@ export function applyAguiEventToTimeline(
|
||||
}
|
||||
ChatTimelineBuilder.appendError(
|
||||
items,
|
||||
event.message || '请求失败',
|
||||
options.runErrorMessage || event.message || '请求失败',
|
||||
metadata(options, state),
|
||||
);
|
||||
ChatTimelineBuilder.finalize(items, {
|
||||
|
||||
Reference in New Issue
Block a user