From a65b8986801b261539015d58a532ca2d20c0f8f5 Mon Sep 17 00:00:00 2001 From: 2910410219 <2910410219@qq.com> Date: Fri, 14 Aug 2026 21:41:32 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BC=9A=E8=AF=9D=E7=9A=84bu?= =?UTF-8?q?g=E5=92=8C=E5=9B=9E=E6=98=BE=E4=BB=A5=E5=8F=8A=E6=BB=9A?= =?UTF-8?q?=E5=8A=A8=E5=8A=A0=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.development | 2 +- src/api/settings/workflow/session.ts | 12 +- src/views/home/components/ChatList.vue | 126 ++++++++++++- src/views/home/components/MainContent.vue | 22 ++- src/views/home/components/SessionResults.vue | 107 ++++------- src/views/home/index.vue | 188 ++++++++++++++----- 6 files changed, 320 insertions(+), 137 deletions(-) diff --git a/.env.development b/.env.development index 524387b..742e0a1 100644 --- a/.env.development +++ b/.env.development @@ -5,4 +5,4 @@ ENV = 'development' # 开发环境走本地代理,避免 CORS VITE_API_URL = 'http://116.204.74.41:8000' # VITE_API_URL = 'http://192.168.3.30:8000' -# VITE_API_URL = 'http://192.168.3.135:8000' \ No newline at end of file +# VITE_API_URL = 'http://192.168.3.135:8000' diff --git a/src/api/settings/workflow/session.ts b/src/api/settings/workflow/session.ts index 8f9a7a4..0c319c5 100644 --- a/src/api/settings/workflow/session.ts +++ b/src/api/settings/workflow/session.ts @@ -19,7 +19,7 @@ export interface VOSessionInfoResult { status: number; // 1-运行中, 2-成功, 3-失败 flowId: string; requestParams: Record; - resultFileUrl: string; + resultContent: string; // 回复文本内容(非文件地址) totalTokens: number; totalFee: number; errorMsg: string; @@ -52,3 +52,13 @@ export function deleteSessionV2(id: string, requestOptions?: RequestOptions) { requestOptions, }); } + +// 删除会话内的对话记录(AI 回复 / 用户问题单条或多条),传 Ids 数组,每项为 { id, type } +export function deleteSessionRecord(ids: { id: number; type: string }[], requestOptions?: RequestOptions) { + return request({ + url: '/ai-agent/session/deleteRecord', + method: 'post', + data: { ids }, + requestOptions, + }); +} diff --git a/src/views/home/components/ChatList.vue b/src/views/home/components/ChatList.vue index e356e41..5340a4c 100644 --- a/src/views/home/components/ChatList.vue +++ b/src/views/home/components/ChatList.vue @@ -1,5 +1,10 @@ diff --git a/src/views/home/index.vue b/src/views/home/index.vue index a746a70..1a9a637 100644 --- a/src/views/home/index.vue +++ b/src/views/home/index.vue @@ -23,11 +23,13 @@ :messages="currentMessages" :results="currentSessionResults" :results-loading="currentSessionResultsLoading" + :has-more="currentSessionHasMore" + :loading-more="currentSessionLoadingMore" @retry="handleRetry" @regenerate="handleRetry" - @preview-result="handlePreviewResult" - @download-result="handleDownloadResult" + @delete="handleDeleteMessage" @workflow-select="handlePlaceWorkflowSelect" + @load-more="handleLoadMore" /> { previewDialogVisible.value = true; }; -const getUrlExt = (url: string): string => { - const m = String(url || '').split('?')[0].toLowerCase().match(/\.([a-z0-9]+)$/); - return m ? m[1] : ''; -}; - -// 会话结果文件预览(复用预览弹窗) -const handlePreviewResult = (url: string) => { - if (!url) return; - previewUrl.value = buildAssetUrl(url); - previewMode.value = getPreviewMode(getUrlExt(url)); - previewDialogVisible.value = true; -}; - -// 会话结果文件下载 -const handleDownloadResult = (url: string) => { - if (!url) return; - const name = String(url).split('/').pop()?.split('?')[0] || 'result'; - downloadAsset(url, name); -}; - // 通用文件下载:优先后端代理,兜底 a 标签 const downloadAsset = async (url: string, name: string) => { try { @@ -360,6 +344,19 @@ const currentSessionResultsLoading = computed(() => { return !!id && !!sessionResultsLoading[id]; }); +// session/get 分页:打开会话详情默认只拿最近 3 条对话,更早的通过滚动加载更多(下一页更早的记录) +const SESSION_PAGE_SIZE = 3; +const sessionPage = reactive>({}); +const sessionLoadingMore = reactive>({}); +const currentSessionHasMore = computed(() => { + const id = activeHistoryId.value; + return !!id && !!sessionPage[id] && !sessionPage[id].done; +}); +const currentSessionLoadingMore = computed(() => { + const id = activeHistoryId.value; + return !!id && !!sessionLoadingMore[id]; +}); + // 占位态:无工作流且无消息(MainContent 显示引导页)时隐藏输入框快捷胶囊,避免与占位卡片重复 const isPlaceholder = computed(() => !selectedWorkflowDetail.value && currentMessages.value.length === 0); @@ -518,7 +515,58 @@ const handleRetry = async (msg: ChatMessage) => { } }; -// ===== 停止生成:发送 workflow_cancel 并关闭连接,保留已生成内容 ===== +// ===== 删除会话内对话记录:删除选中的 AI 回复 + 紧邻其上方的用户问题(同一 record 时去重)===== +const handleDeleteMessage = async (msg: ChatMessage) => { + const sid = activeHistoryId.value; + if (!sid) return; + const list = sessionMessages.value.get(sid) || []; + const idx = list.findIndex((m) => m.id === msg.id); + if (idx < 0) return; + // 收集后端记录 id(type 传记录类型:chat/workflow) + const ids: { id: number; type: string }[] = []; + const pushId = (m: ChatMessage | undefined | null) => { + if (!m?.recordId) return; + const id = Number(m.recordId); + if (!Number.isFinite(id)) return; + if (ids.some((d) => d.id === id)) return; + ids.push({ id, type: m.recordType || 'chat' }); + }; + pushId(msg); + // 向上找最近一条用户消息,一并删除(同一后端记录时自动去重) + for (let i = idx - 1; i >= 0; i--) { + if (list[i].isUser) { + pushId(list[i]); + break; + } + } + if (ids.length === 0) { + ElMessage.warning('该对话记录尚未生成(执行中),可刷新后再删除'); + return; + } + try { + await ElMessageBox.confirm('确定删除该对话记录?此操作不可恢复。', '确认删除', { + confirmButtonText: '删除', + cancelButtonText: '取消', + type: 'warning', + }); + } catch { + return; + } + try { + await deleteSessionRecord(ids); + ElMessage.success('已删除'); + // 删除成功后重新拉取会话结果,消息流与结果卡片同步 + const results = await loadSessionResults(sid); + loadSessionMessagesFromResults(sid, results); + } catch { + // 后端已提示错误,保持原状 + } +}; + +// ===== 停止生成:按执行类型发送对应取消消息并关闭连接,保留已生成内容 ===== +// 普通对话走 agent 协议(启动为 {type:'agent'},取消为 {type:'agent_cancel'}); +// 工作流走 workflow 协议(取消为 {type:'workflow_cancel'})。 +// 判断依据与 handleSend 的分支一致:selectedWorkflowDetail 有值 → 工作流,无值 → 普通对话。 const handleStopGenerate = () => { const sid = activeHistoryId.value; if (!sid || !sendingSessions[sid]) return; @@ -526,7 +574,8 @@ const handleStopGenerate = () => { if (ws && ws.readyState === WebSocket.OPEN) { cancelledWs.add(ws); try { - ws.send(JSON.stringify({ type: 'workflow_cancel' })); + const isWorkflow = !!selectedWorkflowDetail.value; + ws.send(JSON.stringify({ type: isWorkflow ? 'workflow_cancel' : 'agent_cancel' })); } catch { /* 连接异常时忽略,直接关闭 */ } @@ -949,7 +998,7 @@ const handleSelectHistory = async (id: string) => { selectedWorkflowDetail.value = null; isHistoryWorkflow.value = false; inputBarRef.value?.clearWorkflow?.(); - await loadSessionMessagesFromResults(id, results); + loadSessionMessagesFromResults(id, results); } // 清除可重新执行的标记 if (session?.status === 'completed' || session?.status === 'failed' || (session?.status === 'executing' && !sendingSessions[session.id])) { @@ -957,55 +1006,88 @@ const handleSelectHistory = async (id: string) => { } }; -// 加载会话内结果(session/get):workflow+chat 混排,按时间倒序;返回结果列表供判断会话类型 +// 加载会话内结果(session/get 第一页):workflow+chat 混排,按时间倒序 const loadSessionResults = async (sid: string) => { if (!sid) return []; + sessionPage[sid] = { page: 0, total: 0, loading: true, done: false }; sessionResultsLoading[sid] = true; try { - const res: any = await getSessionResults(sid); + const res: any = await getSessionResults(sid, { pageNum: 1, pageSize: SESSION_PAGE_SIZE }); const list = Array.isArray(res?.data?.list) ? res.data.list : Array.isArray(res?.data) ? res.data : []; + const total = Number(res?.data?.total) || 0; + // done:无数据 / 不足一页 / 已一次拿全(后端可能不分页、一次性返回全部,此时不应再触发加载更多) + sessionPage[sid] = { page: 1, total, loading: false, done: total === 0 || list.length < SESSION_PAGE_SIZE || list.length >= total }; sessionResultsMap[sid] = list; return list; } catch { if (!sessionResultsMap[sid]) sessionResultsMap[sid] = []; + sessionPage[sid] = { page: 0, total: 0, loading: false, done: true }; return sessionResultsMap[sid] || []; } finally { sessionResultsLoading[sid] = false; } }; -// 普通对话会话回显:把 session/get 的 chat 结果转换为「问题(用户) + 回复(AI)」消息流,正常对话气泡展示 -const loadSessionMessagesFromResults = async (sid: string, results: VOSessionInfoResult[]) => { +// 把 chat 结果(时间倒序)转成消息流(正序:旧→新)。 +// resultContent 字段即回复文本,直接作为 AI 回复展示,无需再读任何文件。 +const resultsToMessages = (results: VOSessionInfoResult[]): ChatMessage[] => { const chats = (results || []).filter((r) => r.type === 'chat'); const msgs: ChatMessage[] = []; - const pending: { msg: ChatMessage; url: string }[] = []; - // 结果按时间倒序 → 反转成正序,再按记录生成问答 [...chats].reverse().forEach((r) => { const q = r.requestParams?.question; if (q) { - msgs.push({ id: 'rq-' + r.id, content: String(q), time: r.createdAt || '', isUser: true }); + msgs.push({ id: 'rq-' + r.id, content: String(q), time: r.createdAt || '', isUser: true, recordId: String(r.id), recordType: r.type }); } - if (r.resultFileUrl) { - const ai: ChatMessage = { id: 'ra-' + r.id, content: '加载中…', time: r.createdAt || '', isUser: false, loading: true }; - msgs.push(ai); - pending.push({ msg: ai, url: r.resultFileUrl }); + if (r.resultContent) { + msgs.push({ id: 'ra-' + r.id, content: String(r.resultContent), time: r.createdAt || '', isUser: false, recordId: String(r.id), recordType: r.type }); } }); + return msgs; +}; + +// 普通对话会话回显:把会话结果转成正常对话消息流(resultContent 即回复文本) +const loadSessionMessagesFromResults = (sid: string, results: VOSessionInfoResult[]) => { + const msgs = resultsToMessages(results); sessionMessages.value.set(sid, msgs); - // 异步读取 txt 填充 AI 回复 - await Promise.all( - pending.map(async ({ msg, url }) => { - try { - const res = await fetch(getUpFileUrl(url)); - if (!res.ok) throw new Error('HTTP ' + res.status); - msg.content = await res.text(); - } catch { - msg.content = '(回复加载失败)'; - } finally { - msg.loading = false; - } - }) - ); +}; + +// 加载更多(下一页更早的结果):普通对话插入消息流头部,工作流结果追加到列表尾部 +const loadMoreSessionResults = async (sid: string) => { + const st = sessionPage[sid]; + if (!st || st.loading || st.done) return; + const nextPage = st.page + 1; + st.loading = true; + try { + const res: any = await getSessionResults(sid, { pageNum: nextPage, pageSize: SESSION_PAGE_SIZE }); + const list = Array.isArray(res?.data?.list) ? res.data.list : Array.isArray(res?.data) ? res.data : []; + const total = Number(res?.data?.total) || st.total || 0; + st.page = nextPage; + st.total = total; + st.done = list.length === 0 || nextPage * SESSION_PAGE_SIZE >= total; + sessionResultsMap[sid] = [...(sessionResultsMap[sid] || []), ...list]; + // 普通对话:更早的记录转成消息流插入头部 + const msgs = sessionMessages.value.get(sid); + if (msgs) { + const older = resultsToMessages(list); + sessionMessages.value.set(sid, [...older, ...msgs]); + } + return sessionResultsMap[sid]; + } catch { + st.done = true; + return sessionResultsMap[sid] || []; + } finally { + st.loading = false; + } +}; + +const handleLoadMore = async (sid: string | null | undefined) => { + if (!sid || sessionLoadingMore[sid]) return; + sessionLoadingMore[sid] = true; + try { + await loadMoreSessionResults(sid); + } finally { + sessionLoadingMore[sid] = false; + } }; @@ -1054,6 +1136,8 @@ const handleDeleteHistory = async (id: string) => { sessionMessages.value.delete(id); sessionResultsMap[id] = []; delete sessionResultsLoading[id]; + delete sessionPage[id]; + delete sessionLoadingMore[id]; if (activeHistoryId.value === id) { if (historyList.value.length > 0) { activeHistoryId.value = historyList.value[0].id;