fix: 支持模型异常后继续文档对话
- 将本轮文档上下文纳入可持久化的 Agent 用户消息 =- 在异常提示中提供请重试动作并自动发送继续
This commit is contained in:
@@ -1,13 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
actionDisabled?: boolean;
|
||||
actionLabel?: string;
|
||||
message: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
action: [];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="chat-error-notice" role="alert">
|
||||
<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>
|
||||
</template>
|
||||
|
||||
@@ -39,4 +56,41 @@ defineProps<{
|
||||
border: 1px solid currentColor;
|
||||
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>
|
||||
|
||||
@@ -3,6 +3,7 @@ import type {
|
||||
ChatArtifactLoader,
|
||||
ChatDocumentLoader,
|
||||
ChatImageLoader,
|
||||
ChatTimelineErrorItem,
|
||||
ChatTimelineItem as ChatTimelineItemType,
|
||||
ChatTimelineMessageItem,
|
||||
ChatTimelineToolApprovalPayload,
|
||||
@@ -23,6 +24,8 @@ const props = defineProps<{
|
||||
documentLoader?: ChatDocumentLoader;
|
||||
emptyText?: string;
|
||||
emptyTitle?: string;
|
||||
errorAction?: (item: ChatTimelineErrorItem) => string | undefined;
|
||||
errorActionDisabled?: boolean;
|
||||
imageLoader?: ChatImageLoader;
|
||||
items: ChatTimelineItemType[];
|
||||
regenerable?: (item: ChatTimelineMessageItem) => boolean;
|
||||
@@ -33,6 +36,7 @@ const props = defineProps<{
|
||||
const emit = defineEmits<{
|
||||
approve: [payload: ChatTimelineToolApprovalPayload];
|
||||
copyMessage: [item: ChatTimelineMessageItem];
|
||||
errorAction: [item: ChatTimelineErrorItem];
|
||||
regenerateMessage: [item: ChatTimelineMessageItem];
|
||||
reject: [payload: ChatTimelineToolApprovalPayload];
|
||||
selectNextVariant: [item: ChatTimelineMessageItem];
|
||||
@@ -210,6 +214,10 @@ function canRegenerateMessage(item: ChatTimelineItemType) {
|
||||
return item.type === 'message' && (props.regenerable?.(item) ?? false);
|
||||
}
|
||||
|
||||
function errorActionLabel(item: ChatTimelineItemType) {
|
||||
return item.type === 'error' ? props.errorAction?.(item) : undefined;
|
||||
}
|
||||
|
||||
function isAssistantActionAnchor(item: ChatTimelineItemType) {
|
||||
return (
|
||||
item.type === 'message' &&
|
||||
@@ -275,6 +283,8 @@ watch(
|
||||
:copy-action="copyAction"
|
||||
:copyable="copyable"
|
||||
:document-loader="documentLoader"
|
||||
:error-action="errorAction"
|
||||
:error-action-disabled="errorActionDisabled"
|
||||
:image-loader="imageLoader"
|
||||
:items="entry.items"
|
||||
:regenerable="regenerable"
|
||||
@@ -283,6 +293,7 @@ watch(
|
||||
:variant-loading="variantLoading"
|
||||
@approve="emit('approve', $event)"
|
||||
@copy-message="emit('copyMessage', $event)"
|
||||
@error-action="emit('errorAction', $event)"
|
||||
@layout-changed="handleLayoutChanged"
|
||||
@layout-toggle="handleLayoutToggle"
|
||||
@regenerate-message="emit('regenerateMessage', $event)"
|
||||
@@ -306,6 +317,8 @@ watch(
|
||||
:assistant-avatar="assistantAvatar"
|
||||
:item="entry.item"
|
||||
:document-loader="documentLoader"
|
||||
:error-action-disabled="errorActionDisabled"
|
||||
:error-action-label="errorActionLabel(entry.item)"
|
||||
:image-loader="imageLoader"
|
||||
:approval-loading="approvalLoading"
|
||||
:copy-action="copyAction"
|
||||
@@ -315,6 +328,7 @@ watch(
|
||||
:variant-loading="isVariantLoading(entry.item)"
|
||||
@approve="emit('approve', $event)"
|
||||
@copy-message="emit('copyMessage', $event)"
|
||||
@error-action="emit('errorAction', $event)"
|
||||
@regenerate-message="emit('regenerateMessage', $event)"
|
||||
@reject="emit('reject', $event)"
|
||||
@select-next-variant="emit('selectNextVariant', $event)"
|
||||
|
||||
@@ -3,6 +3,7 @@ import type {
|
||||
ChatArtifactLoader,
|
||||
ChatDocumentLoader,
|
||||
ChatImageLoader,
|
||||
ChatTimelineErrorItem,
|
||||
ChatTimelineItem,
|
||||
ChatTimelineMessageItem,
|
||||
ChatTimelineMessagePart,
|
||||
@@ -31,6 +32,8 @@ const props = defineProps<{
|
||||
copyable?: boolean;
|
||||
copyAction?: (item: ChatTimelineMessageItem) => boolean | Promise<boolean>;
|
||||
documentLoader?: ChatDocumentLoader;
|
||||
errorActionDisabled?: boolean;
|
||||
errorActionLabel?: string;
|
||||
imageLoader?: ChatImageLoader;
|
||||
item: ChatTimelineItem;
|
||||
regenerable?: boolean;
|
||||
@@ -41,6 +44,7 @@ const props = defineProps<{
|
||||
const emit = defineEmits<{
|
||||
approve: [payload: ChatTimelineToolApprovalPayload];
|
||||
copyMessage: [item: ChatTimelineMessageItem];
|
||||
errorAction: [item: ChatTimelineErrorItem];
|
||||
regenerateMessage: [item: ChatTimelineMessageItem];
|
||||
reject: [payload: ChatTimelineToolApprovalPayload];
|
||||
selectNextVariant: [item: ChatTimelineMessageItem];
|
||||
@@ -280,7 +284,10 @@ function handleCopyAction() {
|
||||
</div>
|
||||
<ChatErrorNotice
|
||||
v-else-if="item.type === 'error'"
|
||||
:action-disabled="errorActionDisabled"
|
||||
:action-label="errorActionLabel"
|
||||
:message="item.message"
|
||||
@action="emit('errorAction', item)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -3,6 +3,7 @@ import type {
|
||||
ChatArtifactLoader,
|
||||
ChatDocumentLoader,
|
||||
ChatImageLoader,
|
||||
ChatTimelineErrorItem,
|
||||
ChatTimelineItem,
|
||||
ChatTimelineMessageItem,
|
||||
ChatTimelineToolApprovalPayload,
|
||||
@@ -21,6 +22,8 @@ const props = defineProps<{
|
||||
copyable?: (item: ChatTimelineMessageItem) => boolean;
|
||||
copyAction?: (item: ChatTimelineMessageItem) => boolean | Promise<boolean>;
|
||||
documentLoader?: ChatDocumentLoader;
|
||||
errorAction?: (item: ChatTimelineErrorItem) => string | undefined;
|
||||
errorActionDisabled?: boolean;
|
||||
imageLoader?: ChatImageLoader;
|
||||
items: ChatTimelineItem[];
|
||||
regenerable?: (item: ChatTimelineMessageItem) => boolean;
|
||||
@@ -32,6 +35,7 @@ const props = defineProps<{
|
||||
const emit = defineEmits<{
|
||||
approve: [payload: ChatTimelineToolApprovalPayload];
|
||||
copyMessage: [item: ChatTimelineMessageItem];
|
||||
errorAction: [item: ChatTimelineErrorItem];
|
||||
layoutChanged: [];
|
||||
layoutToggle: [roundId: string];
|
||||
regenerateMessage: [item: ChatTimelineMessageItem];
|
||||
@@ -234,6 +238,10 @@ function canRegenerateMessage(item: ChatTimelineItem) {
|
||||
return item.type === 'message' && (props.regenerable?.(item) ?? false);
|
||||
}
|
||||
|
||||
function errorActionLabel(item: ChatTimelineItem) {
|
||||
return item.type === 'error' ? props.errorAction?.(item) : undefined;
|
||||
}
|
||||
|
||||
function isVariantLoading(item: ChatTimelineItem) {
|
||||
return item.type === 'message' && (props.variantLoading?.(item) ?? false);
|
||||
}
|
||||
@@ -313,6 +321,8 @@ function handleNestedLayoutToggle() {
|
||||
"
|
||||
:item="item"
|
||||
:document-loader="documentLoader"
|
||||
:error-action-disabled="errorActionDisabled"
|
||||
:error-action-label="errorActionLabel(item)"
|
||||
:image-loader="imageLoader"
|
||||
:approval-loading="approvalLoading"
|
||||
:copy-action="copyAction"
|
||||
@@ -322,6 +332,7 @@ function handleNestedLayoutToggle() {
|
||||
:variant-loading="isVariantLoading(item)"
|
||||
@approve="emit('approve', $event)"
|
||||
@copy-message="emit('copyMessage', $event)"
|
||||
@error-action="emit('errorAction', $event)"
|
||||
@regenerate-message="emit('regenerateMessage', $event)"
|
||||
@reject="emit('reject', $event)"
|
||||
@select-next-variant="emit('selectNextVariant', $event)"
|
||||
@@ -338,6 +349,8 @@ function handleNestedLayoutToggle() {
|
||||
:artifact-loader="artifactLoader"
|
||||
:item="finalMessage"
|
||||
:document-loader="documentLoader"
|
||||
:error-action-disabled="errorActionDisabled"
|
||||
:error-action-label="errorActionLabel(finalMessage)"
|
||||
:image-loader="imageLoader"
|
||||
:copy-action="copyAction"
|
||||
:copyable="canCopyMessage(finalMessage)"
|
||||
@@ -345,6 +358,7 @@ function handleNestedLayoutToggle() {
|
||||
:regenerate-disabled="regenerateDisabled"
|
||||
:variant-loading="isVariantLoading(finalMessage)"
|
||||
@copy-message="emit('copyMessage', $event)"
|
||||
@error-action="emit('errorAction', $event)"
|
||||
@regenerate-message="emit('regenerateMessage', $event)"
|
||||
@select-next-variant="emit('selectNextVariant', $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 () => {
|
||||
vi.useFakeTimers();
|
||||
const copyAction = vi.fn().mockResolvedValue(true);
|
||||
|
||||
Reference in New Issue
Block a user