fix: 支持模型异常后继续文档对话
- 将本轮文档上下文纳入可持久化的 Agent 用户消息 =- 在异常提示中提供请重试动作并自动发送继续
This commit is contained in:
@@ -423,7 +423,7 @@ public class AgentRunService {
|
|||||||
chatContext.getExt().put(DOCUMENT_CONTEXT_TOKEN_ESTIMATE_EXT_KEY,
|
chatContext.getExt().put(DOCUMENT_CONTEXT_TOKEN_ESTIMATE_EXT_KEY,
|
||||||
documentContext.tokenEstimate());
|
documentContext.tokenEstimate());
|
||||||
String runtimePrompt = effectivePrompt(prompt, !boundDocuments.isEmpty(), !boundMedia.isEmpty());
|
String runtimePrompt = effectivePrompt(prompt, !boundDocuments.isEmpty(), !boundMedia.isEmpty());
|
||||||
AgentMessage userMessage = buildAgentMessage(runtimePrompt, boundMedia);
|
AgentMessage userMessage = buildAgentMessage(runtimePrompt, boundMedia, documentContext);
|
||||||
threadPoolTaskExecutor.execute(() -> startRuntime(
|
threadPoolTaskExecutor.execute(() -> startRuntime(
|
||||||
agent, userMessage, documentContext, account, requestId, traceId, runtimeSessionId,
|
agent, userMessage, documentContext, account, requestId, traceId, runtimeSessionId,
|
||||||
assistantCode, chatContext, runOutput, persistChatlog, runtimeSessionStore, lockHandle));
|
assistantCode, chatContext, runOutput, persistChatlog, runtimeSessionStore, lockHandle));
|
||||||
@@ -646,24 +646,6 @@ public class AgentRunService {
|
|||||||
return agentDocumentService.bindDraft(documentUploads);
|
return agentDocumentService.bindDraft(documentUploads);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 将本轮文档正文追加到临时运行定义的系统提示词中。
|
|
||||||
*
|
|
||||||
* <p>正文只存在于本轮模型调用定义,不写入 chatlog 或 AgentScope 消息记忆。</p>
|
|
||||||
*
|
|
||||||
* @param bundle 临时运行时编译结果
|
|
||||||
* @param documentContext 本轮文档上下文
|
|
||||||
*/
|
|
||||||
private void appendDocumentContext(AgentRuntimeBundle bundle, AgentDocumentContext documentContext) {
|
|
||||||
if (bundle == null || bundle.getDefinition() == null
|
|
||||||
|| documentContext == null || documentContext.text().isBlank()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String current = bundle.getDefinition().getSystemPrompt();
|
|
||||||
bundle.getDefinition().setSystemPrompt(
|
|
||||||
(current == null ? "" : current) + documentContext.text());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 为仅附件输入生成可持久化的最小用户意图。
|
* 为仅附件输入生成可持久化的最小用户意图。
|
||||||
*
|
*
|
||||||
@@ -1206,7 +1188,6 @@ public class AgentRunService {
|
|||||||
}
|
}
|
||||||
AgentRuntimeContext runtimeContext = buildAgentRuntimeContext(chatContext, traceId, runtimeSessionId);
|
AgentRuntimeContext runtimeContext = buildAgentRuntimeContext(chatContext, traceId, runtimeSessionId);
|
||||||
AgentRuntimeBundle bundle = agentRuntimeCompiler.compile(agent, runtimeContext, !persistChatlog);
|
AgentRuntimeBundle bundle = agentRuntimeCompiler.compile(agent, runtimeContext, !persistChatlog);
|
||||||
appendDocumentContext(bundle, documentContext);
|
|
||||||
AgentRuntime runtime = agentRuntimeFactory.create();
|
AgentRuntime runtime = agentRuntimeFactory.create();
|
||||||
// 会话初始化请求
|
// 会话初始化请求
|
||||||
AgentInitRequest request = new AgentInitRequest();
|
AgentInitRequest request = new AgentInitRequest();
|
||||||
@@ -2340,13 +2321,30 @@ public class AgentRunService {
|
|||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
private AgentMessage buildAgentMessage(String prompt, List<AgentBoundMedia> media) {
|
/**
|
||||||
|
* 构建发送给 AgentScope 的用户消息。
|
||||||
|
*
|
||||||
|
* <p>文档正文属于用户提供的不可信材料,作为用户内容块进入本轮模型调用和 AgentScope
|
||||||
|
* memory。聊天记录仍单独保存原始输入与附件引用,页面不会展示正文内容块。</p>
|
||||||
|
*
|
||||||
|
* @param prompt 用户输入
|
||||||
|
* @param media 图片附件
|
||||||
|
* @param documentContext 本轮选中的文档上下文
|
||||||
|
* @return 可持久化的运行时用户消息
|
||||||
|
*/
|
||||||
|
private AgentMessage buildAgentMessage(String prompt,
|
||||||
|
List<AgentBoundMedia> media,
|
||||||
|
AgentDocumentContext documentContext) {
|
||||||
AgentMessage message = new AgentMessage();
|
AgentMessage message = new AgentMessage();
|
||||||
message.setRole(AgentMessageRole.USER);
|
message.setRole(AgentMessageRole.USER);
|
||||||
List<com.easyagents.agent.runtime.message.AgentContentBlock> blocks = new ArrayList<>();
|
List<com.easyagents.agent.runtime.message.AgentContentBlock> blocks = new ArrayList<>();
|
||||||
if (prompt != null && !prompt.isBlank()) {
|
if (prompt != null && !prompt.isBlank()) {
|
||||||
blocks.add(new AgentTextBlock(prompt));
|
blocks.add(new AgentTextBlock(prompt));
|
||||||
}
|
}
|
||||||
|
if (documentContext != null && documentContext.text() != null
|
||||||
|
&& !documentContext.text().isBlank()) {
|
||||||
|
blocks.add(new AgentTextBlock(documentContext.text()));
|
||||||
|
}
|
||||||
if (media != null) {
|
if (media != null) {
|
||||||
for (AgentBoundMedia item : media) {
|
for (AgentBoundMedia item : media) {
|
||||||
AgentMediaBlock image = new AgentMediaBlock("image");
|
AgentMediaBlock image = new AgentMediaBlock("image");
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import com.easyagents.agent.runtime.event.AgentRuntimeEventType;
|
|||||||
import com.easyagents.agent.runtime.message.AgentKnowledgeReference;
|
import com.easyagents.agent.runtime.message.AgentKnowledgeReference;
|
||||||
import com.easyagents.agent.runtime.message.AgentMessage;
|
import com.easyagents.agent.runtime.message.AgentMessage;
|
||||||
import com.easyagents.agent.runtime.message.AgentMessageRole;
|
import com.easyagents.agent.runtime.message.AgentMessageRole;
|
||||||
|
import com.easyagents.agent.runtime.message.AgentTextBlock;
|
||||||
import com.easyagents.agent.runtime.persistence.session.AgentSessionStore;
|
import com.easyagents.agent.runtime.persistence.session.AgentSessionStore;
|
||||||
import com.easyagents.agent.runtime.persistence.session.memory.InMemoryAgentSessionStore;
|
import com.easyagents.agent.runtime.persistence.session.memory.InMemoryAgentSessionStore;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
@@ -69,6 +70,28 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
*/
|
*/
|
||||||
public class AgentRunServiceDraftAndHitlTest {
|
public class AgentRunServiceDraftAndHitlTest {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证文档上下文随用户消息进入可持久化 memory,同时保持独立内容块边界。
|
||||||
|
*
|
||||||
|
* @throws Exception 反射调用失败时抛出
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void buildAgentMessageShouldIncludeDocumentContext() throws Exception {
|
||||||
|
AgentRunService service = new AgentRunService();
|
||||||
|
AgentDocumentContext documentContext = new AgentDocumentContext(
|
||||||
|
"\n<<<DOCUMENT name=\"demo.docx\">>>\n正文\n<<<END_DOCUMENT>>>", 8, List.of());
|
||||||
|
|
||||||
|
AgentMessage message = invoke(service, "buildAgentMessage",
|
||||||
|
new Class<?>[]{String.class, List.class, AgentDocumentContext.class},
|
||||||
|
"请介绍文档", List.of(), documentContext);
|
||||||
|
|
||||||
|
Assert.assertEquals(2, message.getContentBlocks().size());
|
||||||
|
Assert.assertEquals("请介绍文档",
|
||||||
|
((AgentTextBlock) message.getContentBlocks().get(0)).getText());
|
||||||
|
Assert.assertEquals(documentContext.text(),
|
||||||
|
((AgentTextBlock) message.getContentBlocks().get(1)).getText());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建用于 owner 恢复测试的运行描述。
|
* 创建用于 owner 恢复测试的运行描述。
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -181,6 +181,57 @@ describe('agentChatRuntimeManager', () => {
|
|||||||
expect(agentChatRuntimeManager.getSnapshot('101')).toBeUndefined();
|
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 () => {
|
it('刷新恢复已确认输入时重新触发草稿清理回调', async () => {
|
||||||
const account = {
|
const account = {
|
||||||
avatar: '',
|
avatar: '',
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ interface RuntimeSessionState {
|
|||||||
prompt: string;
|
prompt: string;
|
||||||
projectionToolArgs: Record<string, string>;
|
projectionToolArgs: Record<string, string>;
|
||||||
projectionToolNames: Record<string, string>;
|
projectionToolNames: Record<string, string>;
|
||||||
|
retryContextReady: boolean;
|
||||||
runId?: string;
|
runId?: string;
|
||||||
roundId: string;
|
roundId: string;
|
||||||
sending: boolean;
|
sending: boolean;
|
||||||
@@ -65,6 +66,7 @@ interface StoredRuntimeSession {
|
|||||||
prompt: string;
|
prompt: string;
|
||||||
projectionToolArgs?: Record<string, string>;
|
projectionToolArgs?: Record<string, string>;
|
||||||
projectionToolNames?: Record<string, string>;
|
projectionToolNames?: Record<string, string>;
|
||||||
|
retryContextReady?: boolean;
|
||||||
runId?: string;
|
runId?: string;
|
||||||
roundId: string;
|
roundId: string;
|
||||||
sending: boolean;
|
sending: boolean;
|
||||||
@@ -86,10 +88,12 @@ interface StartOptions {
|
|||||||
images?: ChatImageAttachment[];
|
images?: ChatImageAttachment[];
|
||||||
onInputAccepted?: () => Promise<void> | void;
|
onInputAccepted?: () => Promise<void> | void;
|
||||||
prompt: string;
|
prompt: string;
|
||||||
|
retryContextReady?: boolean;
|
||||||
sessionId?: string;
|
sessionId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const STORAGE_VERSION = 5;
|
const STORAGE_VERSION = 6;
|
||||||
|
const MODEL_CONNECTION_ERROR_MESSAGE = '模型连接异常';
|
||||||
const STREAM_NOTIFY_INTERVAL_MS = 50;
|
const STREAM_NOTIFY_INTERVAL_MS = 50;
|
||||||
const STREAM_PERSIST_INTERVAL_MS = 300;
|
const STREAM_PERSIST_INTERVAL_MS = 300;
|
||||||
const MAX_RUNTIME_SESSIONS_PER_IDENTITY = 10;
|
const MAX_RUNTIME_SESSIONS_PER_IDENTITY = 10;
|
||||||
@@ -177,6 +181,7 @@ function persistSession(state: RuntimeSessionState) {
|
|||||||
prompt: state.prompt,
|
prompt: state.prompt,
|
||||||
projectionToolArgs: state.projectionToolArgs,
|
projectionToolArgs: state.projectionToolArgs,
|
||||||
projectionToolNames: state.projectionToolNames,
|
projectionToolNames: state.projectionToolNames,
|
||||||
|
retryContextReady: state.retryContextReady,
|
||||||
runId: state.runId,
|
runId: state.runId,
|
||||||
roundId: state.roundId,
|
roundId: state.roundId,
|
||||||
sending: state.sending,
|
sending: state.sending,
|
||||||
@@ -298,7 +303,7 @@ function restoreSession(identity: string, sessionId: string) {
|
|||||||
}
|
}
|
||||||
const parsed = JSON.parse(raw) as StoredRuntimeSession;
|
const parsed = JSON.parse(raw) as StoredRuntimeSession;
|
||||||
if (
|
if (
|
||||||
![3, 4, STORAGE_VERSION].includes(parsed.version) ||
|
![3, 4, 5, STORAGE_VERSION].includes(parsed.version) ||
|
||||||
parsed.sessionId !== sessionId
|
parsed.sessionId !== sessionId
|
||||||
) {
|
) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -324,6 +329,7 @@ function restoreSession(identity: string, sessionId: string) {
|
|||||||
typeof parsed.projectionToolNames === 'object'
|
typeof parsed.projectionToolNames === 'object'
|
||||||
? parsed.projectionToolNames
|
? parsed.projectionToolNames
|
||||||
: {},
|
: {},
|
||||||
|
retryContextReady: Boolean(parsed.retryContextReady),
|
||||||
runId: parsed.runId,
|
runId: parsed.runId,
|
||||||
roundId: parsed.roundId,
|
roundId: parsed.roundId,
|
||||||
sending: Boolean(parsed.sending && parsed.runId),
|
sending: Boolean(parsed.sending && parsed.runId),
|
||||||
@@ -386,6 +392,7 @@ function acceptInput(
|
|||||||
) {
|
) {
|
||||||
replaceAcceptedAttachments(state.items, state.roundId, payload);
|
replaceAcceptedAttachments(state.items, state.roundId, payload);
|
||||||
state.inputAccepted = true;
|
state.inputAccepted = true;
|
||||||
|
state.retryContextReady = true;
|
||||||
persistSession(state);
|
persistSession(state);
|
||||||
notifyInputAccepted(state);
|
notifyInputAccepted(state);
|
||||||
}
|
}
|
||||||
@@ -563,7 +570,9 @@ function finishRuntimeSuccess(state: RuntimeSessionState) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function finishRuntimeFailure(state: RuntimeSessionState, error: unknown) {
|
function finishRuntimeFailure(state: RuntimeSessionState, error: unknown) {
|
||||||
state.error = errorMessage(error);
|
state.error = state.retryContextReady
|
||||||
|
? MODEL_CONNECTION_ERROR_MESSAGE
|
||||||
|
: errorMessage(error);
|
||||||
state.sending = false;
|
state.sending = false;
|
||||||
state.completed = true;
|
state.completed = true;
|
||||||
const last = state.items[state.items.length - 1];
|
const last = state.items[state.items.length - 1];
|
||||||
@@ -664,7 +673,9 @@ export const agentChatRuntimeManager = {
|
|||||||
event.type === EventType.RUN_ERROR &&
|
event.type === EventType.RUN_ERROR &&
|
||||||
event.code !== 'RUN_CANCELLED'
|
event.code !== 'RUN_CANCELLED'
|
||||||
) {
|
) {
|
||||||
current.error = event.message || '发送失败,请稍后再试';
|
current.error = current.retryContextReady
|
||||||
|
? MODEL_CONNECTION_ERROR_MESSAGE
|
||||||
|
: event.message || '发送失败,请稍后再试';
|
||||||
}
|
}
|
||||||
observeTerminalEvent(current, event.type);
|
observeTerminalEvent(current, event.type);
|
||||||
applyAguiEventToTimeline(
|
applyAguiEventToTimeline(
|
||||||
@@ -674,6 +685,9 @@ export const agentChatRuntimeManager = {
|
|||||||
onInputAccepted(payload) {
|
onInputAccepted(payload) {
|
||||||
acceptInput(current, payload);
|
acceptInput(current, payload);
|
||||||
},
|
},
|
||||||
|
runErrorMessage: current.retryContextReady
|
||||||
|
? MODEL_CONNECTION_ERROR_MESSAGE
|
||||||
|
: undefined,
|
||||||
roundId: current.roundId,
|
roundId: current.roundId,
|
||||||
startedAt: current.startedAt,
|
startedAt: current.startedAt,
|
||||||
},
|
},
|
||||||
@@ -757,6 +771,7 @@ export const agentChatRuntimeManager = {
|
|||||||
prompt: options.prompt,
|
prompt: options.prompt,
|
||||||
projectionToolArgs: {},
|
projectionToolArgs: {},
|
||||||
projectionToolNames: {},
|
projectionToolNames: {},
|
||||||
|
retryContextReady: Boolean(options.retryContextReady),
|
||||||
runId,
|
runId,
|
||||||
roundId,
|
roundId,
|
||||||
sending: true,
|
sending: true,
|
||||||
@@ -794,7 +809,9 @@ export const agentChatRuntimeManager = {
|
|||||||
event.type === EventType.RUN_ERROR &&
|
event.type === EventType.RUN_ERROR &&
|
||||||
event.code !== 'RUN_CANCELLED'
|
event.code !== 'RUN_CANCELLED'
|
||||||
) {
|
) {
|
||||||
current.error = event.message || '发送失败,请稍后再试';
|
current.error = current.retryContextReady
|
||||||
|
? MODEL_CONNECTION_ERROR_MESSAGE
|
||||||
|
: event.message || '发送失败,请稍后再试';
|
||||||
}
|
}
|
||||||
observeTerminalEvent(current, event.type);
|
observeTerminalEvent(current, event.type);
|
||||||
applyAguiEventToTimeline(
|
applyAguiEventToTimeline(
|
||||||
@@ -804,6 +821,9 @@ export const agentChatRuntimeManager = {
|
|||||||
onInputAccepted(payload) {
|
onInputAccepted(payload) {
|
||||||
acceptInput(current, payload);
|
acceptInput(current, payload);
|
||||||
},
|
},
|
||||||
|
runErrorMessage: current.retryContextReady
|
||||||
|
? MODEL_CONNECTION_ERROR_MESSAGE
|
||||||
|
: undefined,
|
||||||
roundId,
|
roundId,
|
||||||
startedAt,
|
startedAt,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import type {
|
import type {
|
||||||
ChatDocumentAttachment,
|
ChatDocumentAttachment,
|
||||||
ChatImageAttachment,
|
ChatImageAttachment,
|
||||||
|
ChatTimelineErrorItem,
|
||||||
ChatTimelineItem,
|
ChatTimelineItem,
|
||||||
ChatTimelineMessageItem,
|
ChatTimelineMessageItem,
|
||||||
ChatTimelineToolApprovalPayload,
|
ChatTimelineToolApprovalPayload,
|
||||||
@@ -108,6 +109,7 @@ const loadingKnowledges = ref(false);
|
|||||||
const savingExtraKnowledges = ref(false);
|
const savingExtraKnowledges = ref(false);
|
||||||
const sending = ref(false);
|
const sending = ref(false);
|
||||||
const runtimeRunning = ref(false);
|
const runtimeRunning = ref(false);
|
||||||
|
const retryableErrorRoundId = ref('');
|
||||||
const approvalLoadingKey = ref('');
|
const approvalLoadingKey = ref('');
|
||||||
const knowledgeOptions = ref<{ label: string; value: string }[]>([]);
|
const knowledgeOptions = ref<{ label: string; value: string }[]>([]);
|
||||||
const knowledgeMap = ref(new Map<string, { id: string; title: string }>());
|
const knowledgeMap = ref(new Map<string, { id: string; title: string }>());
|
||||||
@@ -463,12 +465,19 @@ function syncRuntimeSnapshot(sessionId = currentSessionId.value) {
|
|||||||
: undefined;
|
: undefined;
|
||||||
if (!snapshot) {
|
if (!snapshot) {
|
||||||
sending.value = false;
|
sending.value = false;
|
||||||
|
retryableErrorRoundId.value = '';
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
currentSessionId.value = snapshot.sessionId;
|
currentSessionId.value = snapshot.sessionId;
|
||||||
selectedAgentId.value = String(snapshot.agentId);
|
selectedAgentId.value = String(snapshot.agentId);
|
||||||
timelineItems.value = snapshot.items;
|
timelineItems.value = snapshot.items;
|
||||||
sending.value = snapshot.sending;
|
sending.value = snapshot.sending;
|
||||||
|
retryableErrorRoundId.value =
|
||||||
|
snapshot.terminalOutcome === 'failed' &&
|
||||||
|
snapshot.retryContextReady &&
|
||||||
|
!snapshot.sending
|
||||||
|
? snapshot.roundId
|
||||||
|
: '';
|
||||||
maybeRefreshCompletedRuntimeSession(snapshot);
|
maybeRefreshCompletedRuntimeSession(snapshot);
|
||||||
if (snapshot.prompt && !currentSession.value) {
|
if (snapshot.prompt && !currentSession.value) {
|
||||||
upsertSessionRecord(
|
upsertSessionRecord(
|
||||||
@@ -492,15 +501,20 @@ async function loadConversation(sessionId: string) {
|
|||||||
if (!sessionId) {
|
if (!sessionId) {
|
||||||
timelineItems.value = [];
|
timelineItems.value = [];
|
||||||
sending.value = false;
|
sending.value = false;
|
||||||
|
retryableErrorRoundId.value = '';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const runtimeSnapshot = agentChatRuntimeManager.getSnapshot(sessionId);
|
const runtimeSnapshot = agentChatRuntimeManager.getSnapshot(sessionId);
|
||||||
if (runtimeSnapshot?.sending) {
|
if (
|
||||||
|
runtimeSnapshot?.sending ||
|
||||||
|
runtimeSnapshot?.terminalOutcome === 'failed'
|
||||||
|
) {
|
||||||
syncRuntimeSnapshot(sessionId);
|
syncRuntimeSnapshot(sessionId);
|
||||||
await syncSessionRoute(sessionId);
|
await syncSessionRoute(sessionId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
loadingConversation.value = true;
|
loadingConversation.value = true;
|
||||||
|
retryableErrorRoundId.value = '';
|
||||||
try {
|
try {
|
||||||
const detailRes = await getAgentSession(sessionId);
|
const detailRes = await getAgentSession(sessionId);
|
||||||
const res = await getAgentConversation(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 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 (
|
if (
|
||||||
(!content &&
|
(!content && readyImageCount === 0 && readyDocumentCount === 0) ||
|
||||||
composer.images.readyItems.value.length === 0 &&
|
|
||||||
composer.documents.readyItems.value.length === 0) ||
|
|
||||||
!selectedAgentId.value ||
|
!selectedAgentId.value ||
|
||||||
sending.value
|
sending.value
|
||||||
) {
|
) {
|
||||||
@@ -675,44 +702,54 @@ async function sendContent(rawContent: string) {
|
|||||||
ElMessage.warning('当前回复完成后再发送新消息');
|
ElMessage.warning('当前回复完成后再发送新消息');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (composer.images.uploading.value) {
|
if (includeComposer && composer.images.uploading.value) {
|
||||||
ElMessage.warning('图片上传完成后再发送');
|
ElMessage.warning('图片上传完成后再发送');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (composer.documents.processing.value) {
|
if (includeComposer && composer.documents.processing.value) {
|
||||||
ElMessage.warning('文档读取完成后再发送');
|
ElMessage.warning('文档读取完成后再发送');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const failedImage = composer.images.items.value.find(
|
const failedImage = includeComposer
|
||||||
(item) => item.status === 'error',
|
? composer.images.items.value.find((item) => item.status === 'error')
|
||||||
);
|
: undefined;
|
||||||
if (failedImage) {
|
if (failedImage) {
|
||||||
ElMessage.error(failedImage.error || '请处理上传失败的图片');
|
ElMessage.error(failedImage.error || '请处理上传失败的图片');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const failedDocument = composer.documents.items.value.find(
|
const failedDocument = includeComposer
|
||||||
(item) => item.status === 'error',
|
? composer.documents.items.value.find((item) => item.status === 'error')
|
||||||
);
|
: undefined;
|
||||||
if (failedDocument) {
|
if (failedDocument) {
|
||||||
ElMessage.error(failedDocument.error || '请处理读取失败的文档');
|
ElMessage.error(failedDocument.error || '请处理读取失败的文档');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (includeComposer) {
|
||||||
await composer.flush();
|
await composer.flush();
|
||||||
|
}
|
||||||
sending.value = true;
|
sending.value = true;
|
||||||
|
retryableErrorRoundId.value = '';
|
||||||
try {
|
try {
|
||||||
const sessionId = await agentChatRuntimeManager.start({
|
const sessionId = await agentChatRuntimeManager.start({
|
||||||
agentId: selectedAgentId.value,
|
agentId: selectedAgentId.value,
|
||||||
agentName: selectedAgent.value?.name,
|
agentName: selectedAgent.value?.name,
|
||||||
baseItems: timelineItems.value,
|
baseItems: timelineItems.value,
|
||||||
capabilities: buildCapabilities(),
|
capabilities: buildCapabilities(),
|
||||||
documentUploadIds: composer.documents.uploadIds.value,
|
documentUploadIds: includeComposer
|
||||||
documents: composer.documents.readyItems.value.map((item) => ({
|
? composer.documents.uploadIds.value
|
||||||
...item,
|
: undefined,
|
||||||
})),
|
documents: includeComposer
|
||||||
imageUploadIds: composer.images.uploadIds.value,
|
? composer.documents.readyItems.value.map((item) => ({ ...item }))
|
||||||
images: composer.images.readyItems.value.map((item) => ({ ...item })),
|
: undefined,
|
||||||
onInputAccepted: markComposerInputAccepted,
|
imageUploadIds: includeComposer
|
||||||
|
? composer.images.uploadIds.value
|
||||||
|
: undefined,
|
||||||
|
images: includeComposer
|
||||||
|
? composer.images.readyItems.value.map((item) => ({ ...item }))
|
||||||
|
: undefined,
|
||||||
|
onInputAccepted: includeComposer ? markComposerInputAccepted : undefined,
|
||||||
prompt: content,
|
prompt: content,
|
||||||
|
retryContextReady: Boolean(options.retryContextReady),
|
||||||
sessionId: composer.sessionId.value,
|
sessionId: composer.sessionId.value,
|
||||||
});
|
});
|
||||||
await bindCreatedSession(sessionId, content);
|
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() {
|
async function handleSend() {
|
||||||
await sendContent(promptText.value);
|
await sendContent(promptText.value);
|
||||||
}
|
}
|
||||||
@@ -1266,12 +1321,15 @@ onBeforeUnmount(() => {
|
|||||||
:artifact-loader="loadCurrentAgentArtifact"
|
:artifact-loader="loadCurrentAgentArtifact"
|
||||||
:items="timelineItems"
|
:items="timelineItems"
|
||||||
:document-loader="loadAgentChatDocument"
|
:document-loader="loadAgentChatDocument"
|
||||||
|
:error-action="modelErrorAction"
|
||||||
|
:error-action-disabled="sending || runtimeRunning"
|
||||||
:image-loader="loadAgentChatImage"
|
:image-loader="loadAgentChatImage"
|
||||||
empty-text="选择智能体后开始对话"
|
empty-text="选择智能体后开始对话"
|
||||||
:approval-loading="Boolean(approvalLoadingKey)"
|
:approval-loading="Boolean(approvalLoadingKey)"
|
||||||
:copy-action="handleCopyMessage"
|
:copy-action="handleCopyMessage"
|
||||||
:copyable="canCopyMessage"
|
:copyable="canCopyMessage"
|
||||||
@approve="handleApprove"
|
@approve="handleApprove"
|
||||||
|
@error-action="handleModelErrorRetry"
|
||||||
@reject="handleReject"
|
@reject="handleReject"
|
||||||
@select-next-variant="() => undefined"
|
@select-next-variant="() => undefined"
|
||||||
@select-previous-variant="() => undefined"
|
@select-previous-variant="() => undefined"
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { easyFlowAguiCustomEvent } from './custom-events';
|
|||||||
export interface AguiTimelineProjectionOptions {
|
export interface AguiTimelineProjectionOptions {
|
||||||
finishedAt?: number;
|
finishedAt?: number;
|
||||||
onInputAccepted?: (payload: Record<string, unknown>) => Promise<void> | void;
|
onInputAccepted?: (payload: Record<string, unknown>) => Promise<void> | void;
|
||||||
|
runErrorMessage?: string;
|
||||||
roundId?: string;
|
roundId?: string;
|
||||||
startedAt?: number;
|
startedAt?: number;
|
||||||
}
|
}
|
||||||
@@ -361,7 +362,7 @@ export function applyAguiEventToTimeline(
|
|||||||
}
|
}
|
||||||
ChatTimelineBuilder.appendError(
|
ChatTimelineBuilder.appendError(
|
||||||
items,
|
items,
|
||||||
event.message || '请求失败',
|
options.runErrorMessage || event.message || '请求失败',
|
||||||
metadata(options, state),
|
metadata(options, state),
|
||||||
);
|
);
|
||||||
ChatTimelineBuilder.finalize(items, {
|
ChatTimelineBuilder.finalize(items, {
|
||||||
|
|||||||
@@ -1,13 +1,30 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
defineProps<{
|
defineProps<{
|
||||||
|
actionDisabled?: boolean;
|
||||||
|
actionLabel?: string;
|
||||||
message: string;
|
message: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
action: [];
|
||||||
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="chat-error-notice" role="alert">
|
<div class="chat-error-notice" role="alert">
|
||||||
<span class="chat-error-notice__icon" aria-hidden="true">!</span>
|
<span class="chat-error-notice__icon" aria-hidden="true">!</span>
|
||||||
<span>{{ message }}</span>
|
<span class="chat-error-notice__content">
|
||||||
|
<span>{{ message }}<template v-if="actionLabel">,</template></span>
|
||||||
|
<button
|
||||||
|
v-if="actionLabel"
|
||||||
|
type="button"
|
||||||
|
class="chat-error-notice__action"
|
||||||
|
:disabled="actionDisabled"
|
||||||
|
@click="emit('action')"
|
||||||
|
>
|
||||||
|
{{ actionLabel }}
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -39,4 +56,41 @@ defineProps<{
|
|||||||
border: 1px solid currentColor;
|
border: 1px solid currentColor;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat-error-notice__content {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-error-notice__action {
|
||||||
|
padding: 0;
|
||||||
|
margin-left: var(--space-1);
|
||||||
|
font: inherit;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: inherit;
|
||||||
|
color: currentcolor;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
border-radius: var(--el-border-radius-small);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-error-notice__action:hover:not(:disabled) {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-error-notice__action:active:not(:disabled) {
|
||||||
|
opacity: 0.65;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-error-notice__action:focus-visible {
|
||||||
|
outline: 2px solid var(--el-color-primary-light-3);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-error-notice__action:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type {
|
|||||||
ChatArtifactLoader,
|
ChatArtifactLoader,
|
||||||
ChatDocumentLoader,
|
ChatDocumentLoader,
|
||||||
ChatImageLoader,
|
ChatImageLoader,
|
||||||
|
ChatTimelineErrorItem,
|
||||||
ChatTimelineItem as ChatTimelineItemType,
|
ChatTimelineItem as ChatTimelineItemType,
|
||||||
ChatTimelineMessageItem,
|
ChatTimelineMessageItem,
|
||||||
ChatTimelineToolApprovalPayload,
|
ChatTimelineToolApprovalPayload,
|
||||||
@@ -23,6 +24,8 @@ const props = defineProps<{
|
|||||||
documentLoader?: ChatDocumentLoader;
|
documentLoader?: ChatDocumentLoader;
|
||||||
emptyText?: string;
|
emptyText?: string;
|
||||||
emptyTitle?: string;
|
emptyTitle?: string;
|
||||||
|
errorAction?: (item: ChatTimelineErrorItem) => string | undefined;
|
||||||
|
errorActionDisabled?: boolean;
|
||||||
imageLoader?: ChatImageLoader;
|
imageLoader?: ChatImageLoader;
|
||||||
items: ChatTimelineItemType[];
|
items: ChatTimelineItemType[];
|
||||||
regenerable?: (item: ChatTimelineMessageItem) => boolean;
|
regenerable?: (item: ChatTimelineMessageItem) => boolean;
|
||||||
@@ -33,6 +36,7 @@ const props = defineProps<{
|
|||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
approve: [payload: ChatTimelineToolApprovalPayload];
|
approve: [payload: ChatTimelineToolApprovalPayload];
|
||||||
copyMessage: [item: ChatTimelineMessageItem];
|
copyMessage: [item: ChatTimelineMessageItem];
|
||||||
|
errorAction: [item: ChatTimelineErrorItem];
|
||||||
regenerateMessage: [item: ChatTimelineMessageItem];
|
regenerateMessage: [item: ChatTimelineMessageItem];
|
||||||
reject: [payload: ChatTimelineToolApprovalPayload];
|
reject: [payload: ChatTimelineToolApprovalPayload];
|
||||||
selectNextVariant: [item: ChatTimelineMessageItem];
|
selectNextVariant: [item: ChatTimelineMessageItem];
|
||||||
@@ -210,6 +214,10 @@ function canRegenerateMessage(item: ChatTimelineItemType) {
|
|||||||
return item.type === 'message' && (props.regenerable?.(item) ?? false);
|
return item.type === 'message' && (props.regenerable?.(item) ?? false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function errorActionLabel(item: ChatTimelineItemType) {
|
||||||
|
return item.type === 'error' ? props.errorAction?.(item) : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
function isAssistantActionAnchor(item: ChatTimelineItemType) {
|
function isAssistantActionAnchor(item: ChatTimelineItemType) {
|
||||||
return (
|
return (
|
||||||
item.type === 'message' &&
|
item.type === 'message' &&
|
||||||
@@ -275,6 +283,8 @@ watch(
|
|||||||
:copy-action="copyAction"
|
:copy-action="copyAction"
|
||||||
:copyable="copyable"
|
:copyable="copyable"
|
||||||
:document-loader="documentLoader"
|
:document-loader="documentLoader"
|
||||||
|
:error-action="errorAction"
|
||||||
|
:error-action-disabled="errorActionDisabled"
|
||||||
:image-loader="imageLoader"
|
:image-loader="imageLoader"
|
||||||
:items="entry.items"
|
:items="entry.items"
|
||||||
:regenerable="regenerable"
|
:regenerable="regenerable"
|
||||||
@@ -283,6 +293,7 @@ watch(
|
|||||||
:variant-loading="variantLoading"
|
:variant-loading="variantLoading"
|
||||||
@approve="emit('approve', $event)"
|
@approve="emit('approve', $event)"
|
||||||
@copy-message="emit('copyMessage', $event)"
|
@copy-message="emit('copyMessage', $event)"
|
||||||
|
@error-action="emit('errorAction', $event)"
|
||||||
@layout-changed="handleLayoutChanged"
|
@layout-changed="handleLayoutChanged"
|
||||||
@layout-toggle="handleLayoutToggle"
|
@layout-toggle="handleLayoutToggle"
|
||||||
@regenerate-message="emit('regenerateMessage', $event)"
|
@regenerate-message="emit('regenerateMessage', $event)"
|
||||||
@@ -306,6 +317,8 @@ watch(
|
|||||||
:assistant-avatar="assistantAvatar"
|
:assistant-avatar="assistantAvatar"
|
||||||
:item="entry.item"
|
:item="entry.item"
|
||||||
:document-loader="documentLoader"
|
:document-loader="documentLoader"
|
||||||
|
:error-action-disabled="errorActionDisabled"
|
||||||
|
:error-action-label="errorActionLabel(entry.item)"
|
||||||
:image-loader="imageLoader"
|
:image-loader="imageLoader"
|
||||||
:approval-loading="approvalLoading"
|
:approval-loading="approvalLoading"
|
||||||
:copy-action="copyAction"
|
:copy-action="copyAction"
|
||||||
@@ -315,6 +328,7 @@ watch(
|
|||||||
:variant-loading="isVariantLoading(entry.item)"
|
:variant-loading="isVariantLoading(entry.item)"
|
||||||
@approve="emit('approve', $event)"
|
@approve="emit('approve', $event)"
|
||||||
@copy-message="emit('copyMessage', $event)"
|
@copy-message="emit('copyMessage', $event)"
|
||||||
|
@error-action="emit('errorAction', $event)"
|
||||||
@regenerate-message="emit('regenerateMessage', $event)"
|
@regenerate-message="emit('regenerateMessage', $event)"
|
||||||
@reject="emit('reject', $event)"
|
@reject="emit('reject', $event)"
|
||||||
@select-next-variant="emit('selectNextVariant', $event)"
|
@select-next-variant="emit('selectNextVariant', $event)"
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type {
|
|||||||
ChatArtifactLoader,
|
ChatArtifactLoader,
|
||||||
ChatDocumentLoader,
|
ChatDocumentLoader,
|
||||||
ChatImageLoader,
|
ChatImageLoader,
|
||||||
|
ChatTimelineErrorItem,
|
||||||
ChatTimelineItem,
|
ChatTimelineItem,
|
||||||
ChatTimelineMessageItem,
|
ChatTimelineMessageItem,
|
||||||
ChatTimelineMessagePart,
|
ChatTimelineMessagePart,
|
||||||
@@ -31,6 +32,8 @@ const props = defineProps<{
|
|||||||
copyable?: boolean;
|
copyable?: boolean;
|
||||||
copyAction?: (item: ChatTimelineMessageItem) => boolean | Promise<boolean>;
|
copyAction?: (item: ChatTimelineMessageItem) => boolean | Promise<boolean>;
|
||||||
documentLoader?: ChatDocumentLoader;
|
documentLoader?: ChatDocumentLoader;
|
||||||
|
errorActionDisabled?: boolean;
|
||||||
|
errorActionLabel?: string;
|
||||||
imageLoader?: ChatImageLoader;
|
imageLoader?: ChatImageLoader;
|
||||||
item: ChatTimelineItem;
|
item: ChatTimelineItem;
|
||||||
regenerable?: boolean;
|
regenerable?: boolean;
|
||||||
@@ -41,6 +44,7 @@ const props = defineProps<{
|
|||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
approve: [payload: ChatTimelineToolApprovalPayload];
|
approve: [payload: ChatTimelineToolApprovalPayload];
|
||||||
copyMessage: [item: ChatTimelineMessageItem];
|
copyMessage: [item: ChatTimelineMessageItem];
|
||||||
|
errorAction: [item: ChatTimelineErrorItem];
|
||||||
regenerateMessage: [item: ChatTimelineMessageItem];
|
regenerateMessage: [item: ChatTimelineMessageItem];
|
||||||
reject: [payload: ChatTimelineToolApprovalPayload];
|
reject: [payload: ChatTimelineToolApprovalPayload];
|
||||||
selectNextVariant: [item: ChatTimelineMessageItem];
|
selectNextVariant: [item: ChatTimelineMessageItem];
|
||||||
@@ -280,7 +284,10 @@ function handleCopyAction() {
|
|||||||
</div>
|
</div>
|
||||||
<ChatErrorNotice
|
<ChatErrorNotice
|
||||||
v-else-if="item.type === 'error'"
|
v-else-if="item.type === 'error'"
|
||||||
|
:action-disabled="errorActionDisabled"
|
||||||
|
:action-label="errorActionLabel"
|
||||||
:message="item.message"
|
:message="item.message"
|
||||||
|
@action="emit('errorAction', item)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type {
|
|||||||
ChatArtifactLoader,
|
ChatArtifactLoader,
|
||||||
ChatDocumentLoader,
|
ChatDocumentLoader,
|
||||||
ChatImageLoader,
|
ChatImageLoader,
|
||||||
|
ChatTimelineErrorItem,
|
||||||
ChatTimelineItem,
|
ChatTimelineItem,
|
||||||
ChatTimelineMessageItem,
|
ChatTimelineMessageItem,
|
||||||
ChatTimelineToolApprovalPayload,
|
ChatTimelineToolApprovalPayload,
|
||||||
@@ -21,6 +22,8 @@ const props = defineProps<{
|
|||||||
copyable?: (item: ChatTimelineMessageItem) => boolean;
|
copyable?: (item: ChatTimelineMessageItem) => boolean;
|
||||||
copyAction?: (item: ChatTimelineMessageItem) => boolean | Promise<boolean>;
|
copyAction?: (item: ChatTimelineMessageItem) => boolean | Promise<boolean>;
|
||||||
documentLoader?: ChatDocumentLoader;
|
documentLoader?: ChatDocumentLoader;
|
||||||
|
errorAction?: (item: ChatTimelineErrorItem) => string | undefined;
|
||||||
|
errorActionDisabled?: boolean;
|
||||||
imageLoader?: ChatImageLoader;
|
imageLoader?: ChatImageLoader;
|
||||||
items: ChatTimelineItem[];
|
items: ChatTimelineItem[];
|
||||||
regenerable?: (item: ChatTimelineMessageItem) => boolean;
|
regenerable?: (item: ChatTimelineMessageItem) => boolean;
|
||||||
@@ -32,6 +35,7 @@ const props = defineProps<{
|
|||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
approve: [payload: ChatTimelineToolApprovalPayload];
|
approve: [payload: ChatTimelineToolApprovalPayload];
|
||||||
copyMessage: [item: ChatTimelineMessageItem];
|
copyMessage: [item: ChatTimelineMessageItem];
|
||||||
|
errorAction: [item: ChatTimelineErrorItem];
|
||||||
layoutChanged: [];
|
layoutChanged: [];
|
||||||
layoutToggle: [roundId: string];
|
layoutToggle: [roundId: string];
|
||||||
regenerateMessage: [item: ChatTimelineMessageItem];
|
regenerateMessage: [item: ChatTimelineMessageItem];
|
||||||
@@ -234,6 +238,10 @@ function canRegenerateMessage(item: ChatTimelineItem) {
|
|||||||
return item.type === 'message' && (props.regenerable?.(item) ?? false);
|
return item.type === 'message' && (props.regenerable?.(item) ?? false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function errorActionLabel(item: ChatTimelineItem) {
|
||||||
|
return item.type === 'error' ? props.errorAction?.(item) : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
function isVariantLoading(item: ChatTimelineItem) {
|
function isVariantLoading(item: ChatTimelineItem) {
|
||||||
return item.type === 'message' && (props.variantLoading?.(item) ?? false);
|
return item.type === 'message' && (props.variantLoading?.(item) ?? false);
|
||||||
}
|
}
|
||||||
@@ -313,6 +321,8 @@ function handleNestedLayoutToggle() {
|
|||||||
"
|
"
|
||||||
:item="item"
|
:item="item"
|
||||||
:document-loader="documentLoader"
|
:document-loader="documentLoader"
|
||||||
|
:error-action-disabled="errorActionDisabled"
|
||||||
|
:error-action-label="errorActionLabel(item)"
|
||||||
:image-loader="imageLoader"
|
:image-loader="imageLoader"
|
||||||
:approval-loading="approvalLoading"
|
:approval-loading="approvalLoading"
|
||||||
:copy-action="copyAction"
|
:copy-action="copyAction"
|
||||||
@@ -322,6 +332,7 @@ function handleNestedLayoutToggle() {
|
|||||||
:variant-loading="isVariantLoading(item)"
|
:variant-loading="isVariantLoading(item)"
|
||||||
@approve="emit('approve', $event)"
|
@approve="emit('approve', $event)"
|
||||||
@copy-message="emit('copyMessage', $event)"
|
@copy-message="emit('copyMessage', $event)"
|
||||||
|
@error-action="emit('errorAction', $event)"
|
||||||
@regenerate-message="emit('regenerateMessage', $event)"
|
@regenerate-message="emit('regenerateMessage', $event)"
|
||||||
@reject="emit('reject', $event)"
|
@reject="emit('reject', $event)"
|
||||||
@select-next-variant="emit('selectNextVariant', $event)"
|
@select-next-variant="emit('selectNextVariant', $event)"
|
||||||
@@ -338,6 +349,8 @@ function handleNestedLayoutToggle() {
|
|||||||
:artifact-loader="artifactLoader"
|
:artifact-loader="artifactLoader"
|
||||||
:item="finalMessage"
|
:item="finalMessage"
|
||||||
:document-loader="documentLoader"
|
:document-loader="documentLoader"
|
||||||
|
:error-action-disabled="errorActionDisabled"
|
||||||
|
:error-action-label="errorActionLabel(finalMessage)"
|
||||||
:image-loader="imageLoader"
|
:image-loader="imageLoader"
|
||||||
:copy-action="copyAction"
|
:copy-action="copyAction"
|
||||||
:copyable="canCopyMessage(finalMessage)"
|
:copyable="canCopyMessage(finalMessage)"
|
||||||
@@ -345,6 +358,7 @@ function handleNestedLayoutToggle() {
|
|||||||
:regenerate-disabled="regenerateDisabled"
|
:regenerate-disabled="regenerateDisabled"
|
||||||
:variant-loading="isVariantLoading(finalMessage)"
|
:variant-loading="isVariantLoading(finalMessage)"
|
||||||
@copy-message="emit('copyMessage', $event)"
|
@copy-message="emit('copyMessage', $event)"
|
||||||
|
@error-action="emit('errorAction', $event)"
|
||||||
@regenerate-message="emit('regenerateMessage', $event)"
|
@regenerate-message="emit('regenerateMessage', $event)"
|
||||||
@select-next-variant="emit('selectNextVariant', $event)"
|
@select-next-variant="emit('selectNextVariant', $event)"
|
||||||
@select-previous-variant="emit('selectPreviousVariant', $event)"
|
@select-previous-variant="emit('selectPreviousVariant', $event)"
|
||||||
|
|||||||
@@ -154,6 +154,28 @@ describe('chat timeline toolbar', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders and emits an accessible error action', async () => {
|
||||||
|
const errorItem: Extract<ChatTimelineItem, { type: 'error' }> = {
|
||||||
|
id: 'error-1',
|
||||||
|
message: '模型连接异常',
|
||||||
|
roundId: 'round-1',
|
||||||
|
type: 'error',
|
||||||
|
};
|
||||||
|
const wrapper = mount(ChatTimeline, {
|
||||||
|
props: {
|
||||||
|
errorAction: () => '请重试',
|
||||||
|
items: [errorItem],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const retryButton = wrapper.get('.chat-error-notice__action');
|
||||||
|
expect(wrapper.text()).toContain('模型连接异常,请重试');
|
||||||
|
expect(retryButton.text()).toBe('请重试');
|
||||||
|
await retryButton.trigger('click');
|
||||||
|
|
||||||
|
expect(wrapper.emitted('errorAction')?.[0]?.[0]).toEqual(errorItem);
|
||||||
|
});
|
||||||
|
|
||||||
it('shows a check icon after the copy action succeeds', async () => {
|
it('shows a check icon after the copy action succeeds', async () => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
const copyAction = vi.fn().mockResolvedValue(true);
|
const copyAction = vi.fn().mockResolvedValue(true);
|
||||||
|
|||||||
Reference in New Issue
Block a user