diff --git a/ui/src/api/API_README.md b/ui/src/api/API_README.md index 70cc8d6979c..3a731e97d8f 100644 --- a/ui/src/api/API_README.md +++ b/ui/src/api/API_README.md @@ -465,6 +465,13 @@ Admin 调试接口在 `admin/workspace/conversation.ts`,正式 Chat 接口在 使用各自请求客户端。面板 `common/get-api.ts` 仅按 ChatType 选择完整 API,不拼 URL。 Chat 协议及认证接口见 [chat/README.md](chat/README.md)。 +历史消息和流式聚合复用 `@/api/types` 的 `ChatContent`,定义在 `types/chat-log.ts`; +`ChatLogRecord.question` 与 `messages` 使用该内容项类型,扩展字段保留为 `unknown`。 +Chat、Workspace 调试和 System 调试的记录分页及详情均声明 `ChatLogRecord` 返回类型, +消息列表依赖从 `ConversationApiAdapter` 选取对应接口约定。 +`conversation-panel/core/stream.ts` 的 `ConversationStream` 通过回调推断分片类型, +兼容对话内容项、工具调试和纯文本生成;流读取器使用原生 `Response` 与字节流类型。 + ### 通用文件上传 `admin/file.ts` 与 `chat/file.ts` 分别提供 `postUploadFile(file, sourceId, sourceType, onProgress?)`, diff --git a/ui/src/api/admin/system/resource-management/application/conversation.ts b/ui/src/api/admin/system/resource-management/application/conversation.ts index d81fe97c20a..e642e2aa0d2 100644 --- a/ui/src/api/admin/system/resource-management/application/conversation.ts +++ b/ui/src/api/admin/system/resource-management/application/conversation.ts @@ -1,3 +1,4 @@ +import type { ChatLogRecord, PortalConversationSummary } from '@/api/types' import { get, post, put, del, postStream } from '@/api/admin/core/request' import { ADMIN_API_BASE_PATH as adminApiBase } from '@/api/constants' @@ -16,34 +17,29 @@ const postCancelConversationMessage = (chatId: string, applicationId: string) => /** 恢复对话消息流。 */ const postResumeConversationMessage = (chatId: string, chatRecordId: string, applicationId: string) => - postStream( - adminApiBase, - `${getPrefix(applicationId)}/chat/${chatId}/chat_record/${chatRecordId}/resume_chat_message`, - ) + postStream(adminApiBase, `${getPrefix(applicationId)}/chat/${chatId}/chat_record/${chatRecordId}/resume_chat_message`) /** 获取历史会话分页。 */ const getConversationPage = (page: number, size: number, applicationId: string) => - get(`${getPrefix(applicationId)}/historical_conversation/${page}/${size}`) + get<{ records: PortalConversationSummary[] }>(`${getPrefix(applicationId)}/historical_conversation/${page}/${size}`) /** 获取会话记录详情。 */ const getConversationRecordDetail = (chatId: string, chatRecordId: string, applicationId: string) => - get(`${getPrefix(applicationId)}/chat/${chatId}/chat_record/${chatRecordId}`) + get(`${getPrefix(applicationId)}/chat/${chatId}/chat_record/${chatRecordId}`) /** 获取会话记录分页。 */ const getConversationRecordPage = (chatId: string, page: number, size: number, applicationId: string) => - get(`${getPrefix(applicationId)}/historical_conversation_record/${chatId}/${page}/${size}`) + get<{ records: ChatLogRecord[] }>(`${getPrefix(applicationId)}/historical_conversation_record/${chatId}/${page}/${size}`) /** 删除会话。 */ -const deleteConversation = (chatId: string, applicationId: string) => - del(`${getPrefix(applicationId)}/historical_conversation/${chatId}`) +const deleteConversation = (chatId: string, applicationId: string) => del(`${getPrefix(applicationId)}/historical_conversation/${chatId}`) /** 修改会话信息。 */ const putConversation = (chatId: string, data: unknown, applicationId: string) => put(`${getPrefix(applicationId)}/historical_conversation/${chatId}`, data) /** 使用指定智能体将语音转换为文字。 */ -const postSpeechToText = (applicationId: string, data: unknown) => - post(`${getPrefix(applicationId)}/speech_to_text`, data) +const postSpeechToText = (applicationId: string, data: unknown) => post(`${getPrefix(applicationId)}/speech_to_text`, data) export default { getConversationOpen, diff --git a/ui/src/api/admin/workspace/conversation.ts b/ui/src/api/admin/workspace/conversation.ts index f55047ec514..641575b1e16 100644 --- a/ui/src/api/admin/workspace/conversation.ts +++ b/ui/src/api/admin/workspace/conversation.ts @@ -1,3 +1,4 @@ +import type { ChatLogRecord, PortalConversationSummary } from '@/api/types' import { get, post, put, del, postStream } from '../core/request' import { getWorkspaceId } from '@/utils/resource-context' import { ADMIN_API_BASE_PATH as adminApiBase } from '@/api/constants' @@ -24,22 +25,23 @@ const postResumeConversationMessage = (chatId: string, chatRecordId: string, app const getConversationPage = (page: number, size: number, applicationId?: string) => { const wsId = getWorkspaceId() if (applicationId) { - return get(`/workspace/${wsId}/application/${applicationId}/historical_conversation/${page}/${size}`) + return get<{ records: PortalConversationSummary[] }>(`/workspace/${wsId}/application/${applicationId}/historical_conversation/${page}/${size}`) } - return get(`/workspace/${wsId}/historical_conversation/${page}/${size}`) + return get<{ records: PortalConversationSummary[] }>(`/workspace/${wsId}/historical_conversation/${page}/${size}`) } - const getConversationRecordDetail = (chatId: string, chatRecordId: string, applicationId?: string) => - get(`/workspace/${getWorkspaceId()}/application/${applicationId}/chat/${chatId}/chat_record/${chatRecordId}`) + get(`/workspace/${getWorkspaceId()}/application/${applicationId}/chat/${chatId}/chat_record/${chatRecordId}`) /** 获取会话记录分页。 */ const getConversationRecordPage = (chatId: string, page: number, size: number, applicationId?: string) => { const wsId = getWorkspaceId() if (applicationId) { - return get(`/workspace/${wsId}/application/${applicationId}/historical_conversation_record/${chatId}/${page}/${size}`) + return get<{ records: ChatLogRecord[] }>( + `/workspace/${wsId}/application/${applicationId}/historical_conversation_record/${chatId}/${page}/${size}`, + ) } - return get(`/workspace/${wsId}/historical_conversation_record/${chatId}/${page}/${size}`) + return get<{ records: ChatLogRecord[] }>(`/workspace/${wsId}/historical_conversation_record/${chatId}/${page}/${size}`) } /** 删除会话。 */ diff --git a/ui/src/api/chat/conversation.ts b/ui/src/api/chat/conversation.ts index 10f34b2be25..a71c61b6a81 100644 --- a/ui/src/api/chat/conversation.ts +++ b/ui/src/api/chat/conversation.ts @@ -1,7 +1,7 @@ import { get, post, put, del, postStream } from './core/request' import type { ResponsePage } from './core/types' import { CHAT_API_BASE_PATH as chatApiBase } from '@/api/constants' -import type { PortalConversationSummary } from '@/api/types' +import type { ChatLogRecord, PortalConversationSummary } from '@/api/types' /** v3 对话接口均按智能体划分资源路径。 */ const getApplicationPath = (applicationId: string) => `/v3/application/${applicationId}` @@ -26,25 +26,21 @@ const getConversationPage = (applicationId: string, page: number, size: number) /** 获取门户单个智能体的历史会话分页(懒加载),仅返回摘要,支持摘要模糊搜索。 */ const getPortalConversationPage = (applicationId: string, page: number, size: number, abstract?: string) => - get>( - `/v3/portal/chat/${applicationId}/${page}/${size}`, - abstract ? { abstract } : undefined, - ) + get>(`/v3/portal/chat/${applicationId}/${page}/${size}`, abstract ? { abstract } : undefined) /** 获取单条会话记录详情(含执行详情 execution_details、知识来源、tokens、耗时)。 */ const getConversationRecordDetail = (applicationId: string, chatId: string, chatRecordId: string) => - get(`${getApplicationPath(applicationId)}/chat/${chatId}/chat_record/${chatRecordId}`) + get(`${getApplicationPath(applicationId)}/chat/${chatId}/chat_record/${chatRecordId}`) /** 获取会话记录分页。 */ const getConversationRecordPage = (applicationId: string, chatId: string, page: number, size: number) => - get(`${getApplicationPath(applicationId)}/chat/${chatId}/chat_record/${page}/${size}`) + get<{ records: ChatLogRecord[] }>(`${getApplicationPath(applicationId)}/chat/${chatId}/chat_record/${page}/${size}`) /** 删除会话。 */ const deleteConversation = (applicationId: string, chatId: string) => del(`${getApplicationPath(applicationId)}/chat/${chatId}`) /** 修改会话信息。 */ -const putConversation = (applicationId: string, chatId: string, data: unknown) => - put(`${getApplicationPath(applicationId)}/chat/${chatId}`, data) +const putConversation = (applicationId: string, chatId: string, data: unknown) => put(`${getApplicationPath(applicationId)}/chat/${chatId}`, data) /** 将语音转换为文字。 */ const postSpeechToText = (applicationId: string, data: unknown) => post(`${getApplicationPath(applicationId)}/speech_to_text`, data) diff --git a/ui/src/api/types/chat-log.ts b/ui/src/api/types/chat-log.ts index 7f2cd3ab77b..48aa9161900 100644 --- a/ui/src/api/types/chat-log.ts +++ b/ui/src/api/types/chat-log.ts @@ -24,13 +24,33 @@ export interface ChatLog { update_time: string } +/** 历史消息与流式分片共用的内容项,未约定的扩展字段保留为 unknown。 */ +export interface ChatContent { + type: string + id?: string + content?: string + name?: string + status?: string + arguments?: string + children?: ChatContent[] + workflowRunId?: string + extra?: unknown + form_field_list?: Dict[] + form_content_format?: string + form_data?: Dict + is_submit?: boolean + position?: unknown + chat_record_id?: string + [key: string]: unknown +} + /** 对话日志中的单条问答记录,仅列出前端使用的字段。 */ export interface ChatLogRecord { id: string /** 提问内容项 */ - question: Dict + question: ChatContent /** 回答内容项列表 */ - messages: Dict[] + messages: ChatContent[] message_tokens?: number answer_tokens?: number run_time?: number diff --git a/ui/src/components/COMPONENT_README.md b/ui/src/components/COMPONENT_README.md index 33938d284d7..863907d8a8e 100644 --- a/ui/src/components/COMPONENT_README.md +++ b/ui/src/components/COMPONENT_README.md @@ -228,6 +228,35 @@ Element Plus 的 `v-infinite-scroll`。组件通过 `v-model` 管理已经加载 首次加载或 `reset()` 请求第一页时,组件只显示加载状态;第一页完成且列表为空后才渲染 `empty` 插槽,避免请求过程中短暂显示空状态。已有数据时继续渲染默认插槽,并在触底请求期间保留列表。 +### ReverseInfiniteScroll(对话模块) + +位于 `conversation-panel/core/reverse-infinite-scroll/index.vue`,由对话模块显式导入, +不属于全局组件。内部使用 `el-scrollbar`,父级需要提供受限高度或纵向 Flex 剩余空间, +外层不再额外包裹消息滚动容器。 + +传入 `loading`、`hasMore`、`count` 和 `loadMore: () => Promise`。页码、请求和 +历史数据前插由调用方维护;`loadMore` 在历史数据更新后返回 `true`,失败或没有新增数据返回 +`false`。组件不请求首页;首页由调用方加载,首次出现数据时自动定位到底部,距顶部 60px 时 +串行加载历史。内容不足一屏时继续补页,失败或未新增数据时停止自动补页。 + +默认插槽放消息,`empty` 插槽放欢迎语或空状态,加载期间不显示空状态。 +首次加载使用整块遮罩;有消息时顶部预留状态行,加载期间使用 `mk-conversation-loading` +展示加载动画。失败后可再次主动上滑重试,流式回复状态由消息列表自行渲染。 +历史消息前插后按滚动高度差补偿阅读位置,不要求消息设置额外的 DOM 属性。 +`count` 清零会重置定位状态,切换会话时应通过 +组件 `key` 重建实例,以隔离未结束的补页与定位操作。 + +底部定位与跟随复用 `conversation-panel/core/scroll.ts` 的 `Scroll`;其他消息容器也从此文件导入, +容器卸载或替换时调用 `destroy()` 清理监听和动画帧。 +公开 `scrollToBottom()` 强制定位到底部、`followBottom()` 仅在用户位于底部附近时跟随。 +聊天列表订阅流式进度后调用 `followBottom()`;只读日志无需订阅。 +`message-list` 和 `log-message-list` 使用该容器,保留各自的消息正文与底部信息;聊天列表 +自行展示流式圆点,欢迎语和日志空状态放入 `empty` 插槽。日志列表不注入发送能力。 +消息 Store 的 `loadMore()` 在新增历史记录时返回 `true`,空页、失败或未执行时返回 `false`, +直接传给滚动容器。新会话及重置时 `hasMore` 为 `false`;已有会话主动加载首页,成功后再更新 +`hasMore`。首页失败后通过重新加载首页重试,不从下一页继续。 +Chat、Portal 和 Display 面板直接放置消息列表,并以当前会话 ID 为 `key`,不再包裹额外滚动层。 + ### MkListItem 统一列表行、选中态及悬浮操作区。仅自定义内容时传 `active` 并监听 `click` 即可。 diff --git a/ui/src/components/global/mk-infinite-scroll/index.vue b/ui/src/components/global/mk-infinite-scroll/index.vue index 0c5a05df3f6..ed4d7161da6 100644 --- a/ui/src/components/global/mk-infinite-scroll/index.vue +++ b/ui/src/components/global/mk-infinite-scroll/index.vue @@ -105,9 +105,9 @@ defineExpose({ reset })
-
- 加载中... - 没有更多了 +
+ + 没有更多了
diff --git a/ui/src/conversation-panel/components/content/items/tool/Patch.vue b/ui/src/conversation-panel/components/content/items/tool/Patch.vue index 602ede478bb..5d9b007c703 100644 --- a/ui/src/conversation-panel/components/content/items/tool/Patch.vue +++ b/ui/src/conversation-panel/components/content/items/tool/Patch.vue @@ -68,13 +68,13 @@