diff --git a/ui/src/api/type/application.ts b/ui/src/api/type/application.ts
index 30de8fd1fa2..2b0108d004a 100644
--- a/ui/src/api/type/application.ts
+++ b/ui/src/api/type/application.ts
@@ -548,6 +548,18 @@ export class ChatManagement {
return chatRecord ? chatRecord.is_stop : false
}
+ /**
+ * 获取指定会话中仍在流式输出(尚未写完)的在途消息
+ * 用于切回会话时, 把后台还在跑的流重新接回列表继续实时显示
+ * @param chatId 会话id (chat.chat_id)
+ * @returns 在途的 chat 对象列表
+ */
+ static getActiveByChatId(chatId: string): chatType[] {
+ return Object.values(this.chatMessageContainer)
+ .filter((record) => record.chat.chat_id === chatId && !record.write_ed)
+ .map((record) => record.chat)
+ }
+
/**
* 清除无用数据 也就是被close掉的和stop的数据
*/
diff --git a/ui/src/components/ai-chat/component/answer-content/index.vue b/ui/src/components/ai-chat/component/answer-content/index.vue
index 169db81ea38..e3fd9696ab3 100644
--- a/ui/src/components/ai-chat/component/answer-content/index.vue
+++ b/ui/src/components/ai-chat/component/answer-content/index.vue
@@ -99,7 +99,7 @@
diff --git a/ui/src/components/ai-chat/component/chat-input-operate/index.vue b/ui/src/components/ai-chat/component/chat-input-operate/index.vue
index 15bc8998ae7..d9b9d1a521a 100644
--- a/ui/src/components/ai-chat/component/chat-input-operate/index.vue
+++ b/ui/src/components/ai-chat/component/chat-input-operate/index.vue
@@ -446,14 +446,8 @@ const chatId_context = computed({
emit('update:chatId', v)
},
})
-const localLoading = computed({
- get: () => {
- return props.loading
- },
- set: (v) => {
- emit('update:loading', v)
- },
-})
+// 语音转写的请求 spinner, 独立于 loading prop(loading 现在是父级单向传入的"当前会话生成态")
+const speechLoading = ref(false)
const showURLSetting = ref(false)
const urlForm = reactive({
@@ -807,7 +801,7 @@ const uploadRecording = async (audioBlob: Blob) => {
if (props.applicationDetails.stt_autosend) {
bus.emit('on:transcribing', true)
}
- speechToTextAPI(props.applicationDetails.id as string, formData, localLoading)
+ speechToTextAPI(props.applicationDetails.id as string, formData, speechLoading)
.then((response) => {
inputValue.value = typeof response.data === 'string' ? response.data : ''
// 自动发送
diff --git a/ui/src/components/ai-chat/index.vue b/ui/src/components/ai-chat/index.vue
index 7acbf51d936..f3a0cdddc98 100644
--- a/ui/src/components/ai-chat/index.vue
+++ b/ui/src/components/ai-chat/index.vue
@@ -140,7 +140,7 @@
@@ -310,6 +310,7 @@ const props = withDefaults(
)
const emit = defineEmits([
'refresh',
+ 'openChat',
'scroll',
'openExecutionDetail',
'openParagraph',
@@ -346,6 +347,12 @@ const loading = ref(false)
const inputValue = ref('')
const chartOpenId = ref('')
const chatList = ref([])
+// 当前正在查看的会话是否有在途消息(还在吐字)。
+// 用它驱动"停止回答"按钮、输入禁用、发送拦截, 替代组件级全局 loading,
+// 这样后台其它会话的流式不会把当前会话的输入栏按住。
+const currentChatGenerating = computed(() =>
+ chatList.value.some((c) => c && c.write_ed === false && c.is_stop !== true),
+)
const form_data = ref({})
const api_form_data = ref({})
const userFormRef = ref>()
@@ -531,7 +538,7 @@ function sendMessage(val: string, other_params_data?: any, chat?: chatType): Pro
showUserInput.value = false
- if (!loading.value && props.applicationDetails?.name) {
+ if (!currentChatGenerating.value && props.applicationDetails?.name) {
handleDebounceClick(val, other_params_data, chat)
return true
}
@@ -551,7 +558,7 @@ function sendMessage(val: string, other_params_data?: any, chat?: chatType): Pro
}
} else {
showUserInput.value = false
- if (!loading.value && props.applicationDetails?.name) {
+ if (!currentChatGenerating.value && props.applicationDetails?.name) {
handleDebounceClick(val, other_params_data, chat)
return Promise.resolve(true)
}
@@ -656,6 +663,16 @@ const errorWrite = (chat: any, message?: string) => {
ChatManagement.close(chat.id)
}
+// 停止"当前正在查看的会话"里在途的消息。
+// 只动 chatList(当前会话), 不会波及后台其它正在跑的会话。
+const stopGenerating = () => {
+ chatList.value.forEach((c) => {
+ if (c && c.write_ed === false && c.is_stop !== true) {
+ ChatManagement.stop(c.id)
+ }
+ })
+}
+
// 保存上传文件列表
function chatMessage(chat?: any, problem?: string, re_chat?: boolean, other_params_data?: any) {
@@ -731,6 +748,11 @@ function chatMessage(chat?: any, problem?: string, re_chat?: boolean, other_para
} else if (response.status === 461) {
return Promise.reject(t('aiChat.tip.errorLimitMessage'))
} else {
+ // 新建会话: 此刻后端已执行 set_chat 建好 Chat 行(在产出流之前),
+ // 通知父级把新会话加入历史列表, 这样长回答流式期间切走也能切回来继续看
+ if (props.chatId === 'new') {
+ emit('openChat', chartOpenId.value)
+ }
nextTick(() => {
// 将滚动条滚动到最下面
scrollDiv.value.setScrollTop(getMaxHeight())
@@ -907,11 +929,13 @@ onMounted(() => {
checkAll.value = multipleSelectionChat.value.length === chatList.value.length
emit('update:selection', true)
})
+ bus.on('chat:stop', stopGenerating)
})
onBeforeUnmount(() => {
window.sendMessage = null
window.chatUserProfile = null
+ bus.off('chat:stop', stopGenerating)
})
function setScrollBottom() {
diff --git a/ui/src/views/chat/embed/index.vue b/ui/src/views/chat/embed/index.vue
index dbecb1b9627..1d7d51f0227 100644
--- a/ui/src/views/chat/embed/index.vue
+++ b/ui/src/views/chat/embed/index.vue
@@ -77,6 +77,7 @@
:chatId="currentChatId"
type="ai-chat"
@refresh="refresh"
+ @openChat="refresh"
@scroll="handleScroll"
class="AiChat-embed"
v-model:selection="showSelection"
@@ -107,6 +108,7 @@ import { hexToRgba } from '@/utils/theme'
import { t } from '@/locales'
import ChatHistoryDrawer from './component/ChatHistoryDrawer.vue'
import chatAPI from '@/api/chat/chat'
+import { ChatManagement } from '@/api/type/application'
provide('scrollData', loadInfiniteScroll)
provide('chatLogPagination', () => chatLogPagination)
@@ -223,6 +225,26 @@ function loadInfiniteScroll() {
getChatLog(true)
}
+/**
+ * 切回会话时, 把内存中属于该会话、仍在后台流式输出的在途消息接回列表,
+ * 这样切走时没被打断的流, 切回来能继续实时显示。
+ * - 与 DB 记录 record_id 相同的, 用 live 对象覆盖(否则会显示落库前的空答案)
+ * - DB 里还没有的(尚未落库), 追加到末尾
+ */
+function attachActiveStreams() {
+ const activeChats = ChatManagement.getActiveByChatId(currentChatId.value)
+ if (!activeChats.length) {
+ return
+ }
+ const activeMap = new Map(activeChats.map((chat) => [chat.record_id, chat]))
+ const existIds = new Set(currentRecordList.value.map((v: any) => v.record_id))
+ const merged = currentRecordList.value.map((v: any) =>
+ activeMap.has(v.record_id) ? activeMap.get(v.record_id) : v,
+ )
+ const appendList = activeChats.filter((chat) => !existIds.has(chat.record_id))
+ currentRecordList.value = [...merged, ...appendList]
+}
+
function getChatRecord() {
return chatAPI
.pageChatRecord(
@@ -242,6 +264,7 @@ function getChatRecord() {
a.create_time.localeCompare(b.create_time),
)
if (paginationConfig.current_page === 1) {
+ attachActiveStreams()
nextTick(() => {
// 将滚动条滚动到最下面
AiChatRef.value.setScrollBottom()
diff --git a/ui/src/views/chat/mobile/index.vue b/ui/src/views/chat/mobile/index.vue
index 525d970a494..ce0ea5d4363 100644
--- a/ui/src/views/chat/mobile/index.vue
+++ b/ui/src/views/chat/mobile/index.vue
@@ -77,6 +77,7 @@
:chatId="currentChatId"
type="ai-chat"
@refresh="refresh"
+ @openChat="refresh"
@scroll="handleScroll"
v-model:selection="showSelection"
>
@@ -106,6 +107,7 @@ import useStore from '@/stores'
import { t } from '@/locales'
import ChatHistoryDrawer from './component/ChatHistoryDrawer.vue'
import chatAPI from '@/api/chat/chat'
+import { ChatManagement } from '@/api/type/application'
provide('scrollData', loadInfiniteScroll)
provide('chatLogPagination', () => chatLogPagination)
@@ -227,6 +229,26 @@ function loadInfiniteScroll() {
getChatLog(true)
}
+/**
+ * 切回会话时, 把内存中属于该会话、仍在后台流式输出的在途消息接回列表,
+ * 这样切走时没被打断的流, 切回来能继续实时显示。
+ * - 与 DB 记录 record_id 相同的, 用 live 对象覆盖(否则会显示落库前的空答案)
+ * - DB 里还没有的(尚未落库), 追加到末尾
+ */
+function attachActiveStreams() {
+ const activeChats = ChatManagement.getActiveByChatId(currentChatId.value)
+ if (!activeChats.length) {
+ return
+ }
+ const activeMap = new Map(activeChats.map((chat) => [chat.record_id, chat]))
+ const existIds = new Set(currentRecordList.value.map((v: any) => v.record_id))
+ const merged = currentRecordList.value.map((v: any) =>
+ activeMap.has(v.record_id) ? activeMap.get(v.record_id) : v,
+ )
+ const appendList = activeChats.filter((chat) => !existIds.has(chat.record_id))
+ currentRecordList.value = [...merged, ...appendList]
+}
+
function getChatRecord() {
return chatAPI
.pageChatRecord(
@@ -246,6 +268,7 @@ function getChatRecord() {
a.create_time.localeCompare(b.create_time),
)
if (paginationConfig.current_page === 1) {
+ attachActiveStreams()
nextTick(() => {
// 将滚动条滚动到最下面
AiChatRef.value.setScrollBottom()
diff --git a/ui/src/views/chat/pc/index.vue b/ui/src/views/chat/pc/index.vue
index be23381f9ec..6e6ae2eb232 100644
--- a/ui/src/views/chat/pc/index.vue
+++ b/ui/src/views/chat/pc/index.vue
@@ -172,6 +172,7 @@
:chatId="currentChatId"
executionIsRightPanel
@refresh="refresh"
+ @openChat="refresh"
@scroll="handleScroll"
@open-execution-detail="openExecutionDetail"
@openParagraph="openKnowledgeSource"
@@ -258,6 +259,7 @@ import ExecutionDetailContent from '@/components/ai-chat/component/knowledge-sou
import ParagraphSourceContent from '@/components/ai-chat/component/knowledge-source-component/ParagraphSourceContent.vue'
import ParagraphDocumentContent from '@/components/ai-chat/component/knowledge-source-component/ParagraphDocumentContent.vue'
import HistoryPanel from '@/views/chat/component/HistoryPanel.vue'
+import { ChatManagement } from '@/api/type/application'
import { cloneDeep } from 'lodash'
import { getFileUrl } from '@/utils/common'
import PdfExport from '@/components/pdf-export/index.vue'
@@ -443,6 +445,26 @@ function loadInfiniteScroll() {
getChatLog(true)
}
+/**
+ * 切回会话时, 把内存中属于该会话、仍在后台流式输出的在途消息接回列表,
+ * 这样切走时没被打断的流, 切回来能继续实时显示。
+ * - 与 DB 记录 record_id 相同的, 用 live 对象覆盖(否则会显示落库前的空答案)
+ * - DB 里还没有的(尚未落库), 追加到末尾
+ */
+function attachActiveStreams() {
+ const activeChats = ChatManagement.getActiveByChatId(currentChatId.value)
+ if (!activeChats.length) {
+ return
+ }
+ const activeMap = new Map(activeChats.map((chat) => [chat.record_id, chat]))
+ const existIds = new Set(currentRecordList.value.map((v: any) => v.record_id))
+ const merged = currentRecordList.value.map((v: any) =>
+ activeMap.has(v.record_id) ? activeMap.get(v.record_id) : v,
+ )
+ const appendList = activeChats.filter((chat) => !existIds.has(chat.record_id))
+ currentRecordList.value = [...merged, ...appendList]
+}
+
function getChatRecord() {
return chatAPI
.pageChatRecord(
@@ -462,6 +484,7 @@ function getChatRecord() {
a.create_time.localeCompare(b.create_time),
)
if (paginationConfig.value.current_page === 1) {
+ attachActiveStreams()
nextTick(() => {
// 将滚动条滚动到最下面
AiChatRef.value.setScrollBottom()
diff --git a/ui/src/views/tool/component/ToolListContainer.vue b/ui/src/views/tool/component/ToolListContainer.vue
index 13523c84801..fc74d9005fc 100644
--- a/ui/src/views/tool/component/ToolListContainer.vue
+++ b/ui/src/views/tool/component/ToolListContainer.vue
@@ -695,6 +695,9 @@ function openEditDialog(data?: any) {
checkAll.value = multipleSelection.value.length === tool.toolList.length
return
}
+ if (!permissionPrecise.value.edit(data?.id)) {
+ return
+ }
// 有template_id的不允许编辑,是模板转换来的
if (data?.template_id) {
return