Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions ui/src/api/API_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<Chunk>` 通过回调推断分片类型,
兼容对话内容项、工具调试和纯文本生成;流读取器使用原生 `Response` 与字节流类型。

### 通用文件上传

`admin/file.ts` 与 `chat/file.ts` 分别提供 `postUploadFile(file, sourceId, sourceType, onProgress?)`,
Expand Down
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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<ChatLogRecord>(`${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,
Expand Down
14 changes: 8 additions & 6 deletions ui/src/api/admin/workspace/conversation.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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<ChatLogRecord>(`/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}`)
}

/** 删除会话。 */
Expand Down
14 changes: 5 additions & 9 deletions ui/src/api/chat/conversation.ts
Original file line number Diff line number Diff line change
@@ -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}`
Expand All @@ -26,25 +26,21 @@ const getConversationPage = (applicationId: string, page: number, size: number)

/** 获取门户单个智能体的历史会话分页(懒加载),仅返回摘要,支持摘要模糊搜索。 */
const getPortalConversationPage = (applicationId: string, page: number, size: number, abstract?: string) =>
get<ResponsePage<PortalConversationSummary>>(
`/v3/portal/chat/${applicationId}/${page}/${size}`,
abstract ? { abstract } : undefined,
)
get<ResponsePage<PortalConversationSummary>>(`/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<ChatLogRecord>(`${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)
Expand Down
24 changes: 22 additions & 2 deletions ui/src/api/types/chat-log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<unknown>[]
form_content_format?: string
form_data?: Dict<unknown>
is_submit?: boolean
position?: unknown
chat_record_id?: string
[key: string]: unknown
}

/** 对话日志中的单条问答记录,仅列出前端使用的字段。 */
export interface ChatLogRecord {
id: string
/** 提问内容项 */
question: Dict<unknown>
question: ChatContent
/** 回答内容项列表 */
messages: Dict<unknown>[]
messages: ChatContent[]
message_tokens?: number
answer_tokens?: number
run_time?: number
Expand Down
29 changes: 29 additions & 0 deletions ui/src/components/COMPONENT_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean>`。页码、请求和
历史数据前插由调用方维护;`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` 即可。
Expand Down
6 changes: 3 additions & 3 deletions ui/src/components/global/mk-infinite-scroll/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,9 @@ defineExpose({ reset })
<div v-loading="loading && !dataList.length" class="min-h-full">
<slot v-if="dataList.length" />
<slot v-else-if="firstPageLoaded" name="empty" />
<div ref="triggerRef" aria-live="polite" class="flex-center py-4 shrink-0 text-sm text-N600">
<span v-if="loading && dataList.length">加载中...</span>
<span v-else-if="showFinishedText">没有更多了</span>
<div ref="triggerRef" aria-live="polite" class="flex-center py-4 shrink-0">
<span v-if="loading && dataList.length"><span class="mk-conversation-loading" /></span>
<span v-else-if="showFinishedText" class="text-sm text-N600">没有更多了</span>
</div>
</div>
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -68,20 +68,21 @@
</template>

<script setup lang="ts">
import { computed, reactive, ref, watch, nextTick, onMounted } from 'vue'
import { computed, reactive, ref, watch, nextTick, onMounted, onBeforeUnmount } from 'vue'
import Loading from '@/conversation-panel/loading/index.vue'
import ScrollDiff from './components/ScrollDiff.vue'
import { parsePatch } from './patch/parsePatch'
import type { PatchFileStatus } from './patch/patchTypes'
import { extractPartialJsonField } from './extract-partial-json'
import { Scroll } from '@/conversation-panel/core/stream'
import { Scroll } from '@/conversation-panel/core/scroll'

const props = defineProps<{ content: any; loading: boolean; expanded: boolean }>()

const multiScrollRef = ref<HTMLElement>()
let multiScroll: Scroll | null = null

function initScroll() {
multiScroll?.destroy()
if (multiScrollRef.value) {
multiScroll = new Scroll(multiScrollRef.value)
multiScroll.forceBottom()
Expand All @@ -99,11 +100,14 @@ watch(
await nextTick()
initScroll()
} else {
multiScroll?.destroy()
multiScroll = null
}
},
)

onBeforeUnmount(() => multiScroll?.destroy())

const patchDiff = computed(() => {
const raw = props.content.arguments
if (!raw) return ''
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
</template>

<script setup lang="ts">
import { ref, watch, onMounted, nextTick } from 'vue'
import { ref, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
import FileDiff from '../patch/FileDiff.vue'
import type { PatchFileInfo } from '../patch/patchTypes'
import { Scroll } from '@/conversation-panel/core/stream'
import { Scroll } from '@/conversation-panel/core/scroll'

const props = defineProps<{ file: PatchFileInfo }>()

Expand All @@ -22,6 +22,8 @@ onMounted(() => {
}
})

onBeforeUnmount(() => scroll?.destroy())

watch(
() => props.file.additions,
() => {
Expand Down
14 changes: 8 additions & 6 deletions ui/src/conversation-panel/components/log-message-list/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ export function createLogMessageListStore(deps: LogMessageListDeps) {
const messages = ref<ChatMessage[]>([])
const loading = ref(false)

const pageSize = 20
const pageSize = 30
const currentPage = ref(1)
const hasMore = ref(true)
const hasMore = ref(false)

// 接口按最新在前分页:页内 reverse 成正序,更旧的页拼到前面
const loadMessages = async (cid: string, page = 1) => {
Expand All @@ -47,8 +47,10 @@ export function createLogMessageListStore(deps: LogMessageListDeps) {
messages.value = page === 1 ? result : [...result, ...messages.value]
currentPage.value = page
hasMore.value = records.length >= pageSize
return records.length > 0
} catch {
// 静默处理
return false
} finally {
loading.value = false
}
Expand All @@ -59,15 +61,15 @@ export function createLogMessageListStore(deps: LogMessageListDeps) {
const cid = getChatId()
messages.value = []
currentPage.value = 1
hasMore.value = true
hasMore.value = false
if (!cid) return
await loadMessages(cid, 1)
}

const loadMore = async () => {
const loadMore = async (): Promise<boolean> => {
const cid = getChatId()
if (!hasMore.value || loading.value || !cid) return
await loadMessages(cid, currentPage.value + 1)
if (!hasMore.value || loading.value || !cid) return false
return loadMessages(cid, currentPage.value + 1)
}

return { messages, loading, hasMore, load, loadMore }
Expand Down
Loading
Loading