diff --git a/src/views/home/components/ChatList.vue b/src/views/home/components/ChatList.vue index 4a83a8a..33e7734 100644 --- a/src/views/home/components/ChatList.vue +++ b/src/views/home/components/ChatList.vue @@ -60,6 +60,7 @@ string; } const props = withDefaults(defineProps(), { hasMore: false, loadingMore: false, + resolveUrl: undefined, }); const emit = defineEmits(); const chatListRef = ref(null); @@ -201,6 +205,11 @@ const thinkingTitle = (msg: ChatMessage): string => { // autoLoading 标记本次 load-more 是否由"自动续载"触发:自动续载不保持位置(落在最新),用户上滑保持视口 let autoLoading = false; +// 自动续载累计次数上限:连续自动加载超过上限即停,防止异常数据(如 hasMore 恒 true 且视口不满) +// 导致 load-more 无限循环占满主线程。用户主动上滑加载不受此上限约束(见 handleScroll)。 +const MAX_AUTO_LOAD = 5; +let autoLoadCount = 0; +const canAutoLoad = (): boolean => autoLoadCount < MAX_AUTO_LOAD; // 消息新增或内容变化时自动滚动到底部;顶部加载更多期间不滚底。 // immediate:切换会话时 ChatList 会重建,挂载即定位到最新消息,避免停留在顶部旧消息。 @@ -214,8 +223,14 @@ watch( if (!el) return; el.scrollTop = el.scrollHeight; if (props.messages.length > 0 && props.hasMore && el.scrollHeight <= el.clientHeight) { - autoLoading = true; - emit('load-more'); + if (canAutoLoad()) { + autoLoading = true; + autoLoadCount++; + emit('load-more'); + } else { + // 达到自动续载上限:停止自动加载,避免无限循环;用户可主动上滑继续加载更早记录 + autoLoading = false; + } } }); }, @@ -244,7 +259,13 @@ watch( // 恢复时须把 scrollTop 设回底部定位到最新消息;若仍不满屏则继续自动续载 el.scrollTop = el.scrollHeight; if (props.messages.length > 0 && props.hasMore && el.scrollHeight <= el.clientHeight) { - emit('load-more'); + if (canAutoLoad()) { + autoLoadCount++; + emit('load-more'); + } else { + // 达到自动续载上限:停止自动加载,避免无限循环 + autoLoading = false; + } } else { autoLoading = false; } @@ -262,7 +283,9 @@ const handleScroll = (e: Event) => { const el = chatListRef.value; if (!el || !props.hasMore || props.loadingMore) return; if (el.scrollTop <= 4) { + // 用户主动上滑加载更早:重置自动续载计数,使用户操作不受自动续载上限约束 autoLoading = false; + autoLoadCount = 0; emit('load-more'); } }; diff --git a/src/views/home/components/WorkflowOutputCard.vue b/src/views/home/components/WorkflowOutputCard.vue index 6eb9e38..45b7a6e 100644 --- a/src/views/home/components/WorkflowOutputCard.vue +++ b/src/views/home/components/WorkflowOutputCard.vue @@ -1,22 +1,62 @@ @@ -33,6 +73,8 @@ interface WorkflowOutput { interface Props { outputs: WorkflowOutput[]; + // 相对路径 → 完整 URL 解析函数(由父级传入 buildAssetUrl),缺省时直接用原 url(绝对地址场景) + resolveUrl?: (url: string) => string; } interface Emits { @@ -58,20 +100,76 @@ const getIcon = (type: string) => { .workflow-output-card { display: flex; flex-direction: column; - gap: 6px; + gap: 8px; margin-top: 10px; - background: #f8fafc; - border: 1px solid #f1f5f9; - border-radius: 10px; - padding: 8px; } +// 媒体产出(图片/视频/音频):常驻内嵌展示,hover 浮出下载/删除操作 +.output-media { + position: relative; + overflow: hidden; + border-radius: 10px; + background: #0f172a; + + img, + video { + display: block; + width: 100%; + max-height: 320px; + object-fit: contain; + background: #0f172a; + } + + audio { + display: block; + width: 100%; + background: #0f172a; + padding: 10px; + } + + .media-name { + position: absolute; + left: 0; + right: 0; + bottom: 0; + padding: 4px 8px; + font-size: 12px; + color: #e2e8f0; + background: linear-gradient(transparent, rgba(0, 0, 0, 0.6)); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .media-actions { + position: absolute; + top: 6px; + right: 6px; + display: flex; + gap: 2px; + padding: 2px; + border-radius: 8px; + background: rgba(15, 23, 42, 0.75); + opacity: 0; + transition: opacity 0.15s; + } + + &:hover { + .media-actions { + opacity: 1; + } + } +} + +// 非媒体产出:文件行 + 操作按钮 .output-item { display: flex; align-items: center; gap: 8px; - padding: 6px 8px; - border-radius: 8px; + padding: 8px 10px; + background: #f8fafc; + border: 1px solid #f1f5f9; + border-radius: 10px; transition: background 0.15s; &:hover { @@ -145,4 +243,21 @@ const getIcon = (type: string) => { font-size: 13px; } } + +// 媒体产出内的操作按钮:深色背景上使用浅色图标 +.output-media { + .output-action { + color: #cbd5e1; + + &:hover { + background: rgba(255, 255, 255, 0.18); + color: #f8fafc; + } + + &.is-danger:hover { + background: rgba(220, 38, 38, 0.35); + color: #fecaca; + } + } +} diff --git a/src/views/home/index.vue b/src/views/home/index.vue index e09ab6b..cb45f0e 100644 --- a/src/views/home/index.vue +++ b/src/views/home/index.vue @@ -31,6 +31,7 @@ @output-preview="handleOutputPreview" @output-download="handleOutputDownload" @output-delete="handleOutputDelete" + :resolve-url="buildAssetUrl" @workflow-select="handlePlaceWorkflowSelect" @load-more="handleLoadMore" /> @@ -90,7 +91,7 @@ import MainContent from './components/MainContent.vue'; import InputBar from './components/InputBar.vue'; import TemplateCompleteDialog from './components/TemplateCompleteDialog.vue'; import SessionModelSetter from './components/SessionModelSetter.vue'; -import { applyHomeFormValues, extractWorkflowOutputs } from './utils/flowDsl'; +import { applyHomeFormValues, buildOutputsFromUrls, extractWorkflowOutputs } from './utils/flowDsl'; import type { WorkflowOutput } from './utils/flowDsl'; import { getChatModel, listModelManage } from '/@/api/settings/modelConfigV2'; import { connectSessionSocket, sendAgentStart, sendWorkflowStart, sendCancel } from './utils/wsExecute'; @@ -432,9 +433,12 @@ const currentSessionResults = computed(() => { const SESSION_PAGE_SIZE = 10; const sessionPage = reactive>({}); const sessionLoadingMore = reactive>({}); +// 会话是否已有后端正式号:已认领(后端生成的 UUID)→ true;未认领的本地临时会话(virtual_ 前缀)→ false +const hasBackendSessionId = (sid?: string): boolean => !!sid && !String(sid).startsWith('virtual_'); const currentSessionHasMore = computed(() => { const id = activeHistoryId.value; - return !!id && !!sessionPage[id] && !sessionPage[id].done; + // 未认领后端正式号的本地临时会话无历史记录,不提供加载更多,避免触发自动续载空转 + return !!id && hasBackendSessionId(id) && !!sessionPage[id] && !sessionPage[id].done; }); const currentSessionLoadingMore = computed(() => { const id = activeHistoryId.value; @@ -444,8 +448,71 @@ const currentSessionLoadingMore = computed(() => { // 占位态:无工作流且无消息(MainContent 显示引导页)时隐藏输入框快捷胶囊,避免与占位卡片重复 const isPlaceholder = computed(() => !selectedWorkflowDetail.value && currentMessages.value.length === 0); -const getSessionId = () => { - return `session_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`; +// 建连会话号:已认领(后端正式 UUID,非 virtual_ 本地临时号)回传复用;未认领传空让后端创建,首帧 ack 认领 +const resolveSessionIdForConnect = (s: any): string => { + const sid = s?.sessionId || ''; + return hasBackendSessionId(sid) ? sid : ''; +}; + +// 会话 id 别名:认领后本地临时 id → 后端正式号(historyList 条目已就地更新,这里保留旧→新映射供进行中的轮解析) +const sessionIdAlias: Record = {}; +const currentIdOf = (sid: string): string => sessionIdAlias[sid] || sid; + +// 认领后端正式会话号:把本地临时会话(virtual_ 前缀)就地迁移为后端 UUID, +// 所有以旧 id 为 key 的状态同步迁移,运行中消息流随 activeHistoryId 落到新 key +const claimSessionId = (oldId: string, newId: string) => { + if (!oldId || !newId || oldId === newId) return; + // 历史列表条目就地更新(同对象引用,进行中闭包读到的 id 同步最新) + const item = historyList.value.find((h) => h.id === oldId); + if (item) { + item.id = newId; + item.sessionId = newId; + } + sessionIdAlias[oldId] = newId; + // 消息列表 + const msgs = sessionMessages.value.get(oldId); + if (msgs) { + sessionMessages.value.delete(oldId); + sessionMessages.value.set(newId, msgs); + } + // 分页 / 结果状态 + if (sessionResultsMap[oldId] !== undefined) { + sessionResultsMap[newId] = sessionResultsMap[oldId]; + delete sessionResultsMap[oldId]; + } + if (sessionResultsLoading[oldId] !== undefined) { + sessionResultsLoading[newId] = sessionResultsLoading[oldId]; + delete sessionResultsLoading[oldId]; + } + if (sessionPage[oldId]) { + sessionPage[newId] = sessionPage[oldId]; + delete sessionPage[oldId]; + } + if (sessionLoadingMore[oldId] !== undefined) { + sessionLoadingMore[newId] = sessionLoadingMore[oldId]; + delete sessionLoadingMore[oldId]; + } + // 发送中标记 / 活跃会话指向 + if (sendingSessions[oldId] !== undefined) { + sendingSessions[newId] = sendingSessions[oldId]; + delete sendingSessions[oldId]; + } + if (activeHistoryId.value === oldId) activeHistoryId.value = newId; + // 连接状态对齐 + if (wsState && wsState.sid === oldId) wsState.sid = newId; +}; + +// 认领后刷新会话标题:用后端正式号查一次会话列表,把「新会话 N」更新为后端返回的正式名(fire-and-forget) +const refreshSessionTitle = async (uuid: string) => { + try { + const sessionRes = await getSessionListV2({ pageNum: 1, pageSize: 10 }); + const fresh = getSessionData(sessionRes).find((s: any) => String(s.sessionId) === uuid); + if (!fresh) return; + const item = historyList.value.find((h) => h.id === uuid); + if (item) item.title = fresh.sessionName || '未命名会话'; + } catch { + /* 静默:标题保持本地临时名,下次列表刷新自然更新 */ + } }; const handleWorkflowSelect = async (workflowId: string | null, isTemplate?: boolean) => { @@ -604,6 +671,13 @@ const ensureSocket = async (sid: string, sessionId: string): Promise { if (wsState?.ws === session?.ws) activeHandler?.onMessage(raw); }, + onClaim: (uuid) => { + // 首次建连:后端在 ack 里返回正式会话号 → 认领本地临时会话,后续所有操作以正式号为 key + if (wsState?.ws === session?.ws) { + claimSessionId(sid, uuid); + void refreshSessionTitle(uuid); + } + }, onError: (ev) => { if (wsState?.ws === session?.ws) activeHandler?.onError(ev); }, @@ -689,7 +763,7 @@ const handleSend = async (message: string) => { curSession.status = 'executing'; - const sessionId = curSession.sessionId || getSessionId(); + const sessionId = resolveSessionIdForConnect(curSession); // 工作流执行由表单卡片提交触发(handleFormSubmit);输入框发送恒走普通对话 await runChat(sid, sessionId, message); @@ -727,7 +801,7 @@ const handleRetry = async (msg: ChatMessage) => { list.splice(idx, 1); sendingSessions[sid] = true; session.status = 'executing'; - const sessionId = session.sessionId || getSessionId(); + const sessionId = resolveSessionIdForConnect(session); try { await runChat(sid, sessionId, question); } catch { @@ -820,7 +894,7 @@ const runWorkflow = async ( } let finished = false; - const finishExec = (success: boolean, errorMsg?: string) => { + const finishExec = (success: boolean, errorMsg?: string, outputs?: WorkflowOutput[]) => { if (finished) return; finished = true; // 定格表单卡片:done/failed + 失败信息;执行结束清进度(避免残留影响重新编辑) @@ -828,17 +902,18 @@ const runWorkflow = async ( formMsg.formError = success ? undefined : errorMsg || '执行失败'; formMsg.formProgress = undefined; curSession.status = success ? 'completed' : 'failed'; - // 结果消息:成功记录后续异步附加本次执行产出(outputs) + // 结果消息:成功时携带本次执行产出(outputs),直接以产出卡片渲染在对话里。 + // 产出来源优先 flow_complete 事件自带 resultFileUrls(buildOutputsFromUrls),异步详情链路作兜底 const resultMsg: ChatMessage = { id: 'msg-' + Date.now() + (success ? '-done' : '-fail'), - content: success ? '✅ 执行完成,可前往工作空间查看产出' : `❌ ${errorMsg || '执行失败,请重试或联系管理员'}`, + content: success ? '✅ 执行完成' : `❌ ${errorMsg || '执行失败,请重试或联系管理员'}`, time: formatTime(new Date()), isUser: false, - outputs: [], + outputs: outputs || [], }; addMessage(resultMsg); if (success) { - ElMessage.success('✅ 执行完成,可前往工作空间查看'); + ElMessage.success('✅ 执行完成'); // 刷新工作空间树(查询所有结果) getExecutionList().then((res) => { imgAddressPrefix.value = res.data?.imgAddressPrefix || ''; @@ -846,6 +921,7 @@ const runWorkflow = async ( }); // 会话内渲染本次产出:拉最新 session/get 记录(时间倒序首条)→ getExecutionDetail → 附加到结果消息 // 时序降级:记录未及时写入则静默跳过(用户重进会话可见回显产出) + // 会话在建连时已认领为后端正式号,直接按认领后的 sid 查询本次执行记录 loadSessionResults(sid).then((results) => { const latest = Array.isArray(results) && results.length ? results[0] : undefined; if (latest && latest.type === 'workflow' && !latest.errorMsg) { @@ -860,36 +936,8 @@ const runWorkflow = async ( } else { ElMessage.error(errorMsg || '执行失败,请重试'); } - // 虚拟会话执行后刷新会话列表替换真实条目 - if (curSession.id.startsWith('virtual_')) { - getSessionListV2({ pageNum: 1, pageSize: 10 }).then((sessionRes) => { - const freshList = getSessionData(sessionRes) - .filter((s: any) => s.sessionId) - .map((s: any) => ({ - id: String(s.sessionId), - sessionId: String(s.sessionId), - title: s.sessionName || '未命名会话', - time: s.createdAt?.substring(0, 10) || '', - })); - const match = freshList.find((s: any) => s.sessionId === curSession.sessionId); - if (match) { - const idx = historyList.value.findIndex((h) => h.id === curSession.id); - if (idx >= 0) { - const msgs = sessionMessages.value.get(curSession.id); - sessionMessages.value.delete(curSession.id); - sessionMessages.value.set(match.id, msgs || []); - historyList.value[idx] = { ...match, status: curSession.status }; - if (activeHistoryId.value === curSession.id) { - activeHistoryId.value = match.id; - } - } - loadSessionResults(match.id); - } - }); - } else { - // 非虚拟会话:执行完成/失败后直接刷新会话结果 - loadSessionResults(sid); - } + // 会话在建连时已认领为后端正式号(UUID),执行完成/失败后直接刷新会话结果 + loadSessionResults(sid); delete sendingSessions[sid]; // 执行完成后切回对话页:清空工作流选择,主区域展示消息流 if (success) { @@ -920,7 +968,11 @@ const runWorkflow = async ( if (msg) { if (msg.type === 'error') { finishExec(false, getErrorText(msg)); + } else if (msg.type === 'flow_complete') { + // flow_complete 为后端新增的工作流完成信号,事件自带产出文件 URL 列表 → 直接渲染产出卡片 + finishExec(true, undefined, buildOutputsFromUrls(msg?.data?.resultFileUrls)); } else if (msg.type === 'answer') { + // answer 为最终回答完成信号(无产出文件) finishExec(true); } else { // 工作流节点进度推进:node_start / node_complete → 更新表单卡片运行态进度文本 @@ -961,6 +1013,9 @@ const runWorkflow = async ( finishExec(false, 'WebSocket 初始化失败,请检查服务地址'); return; } + // 首次建连已认领后端正式号(ensureSocket 就绪前 ack 已处理):本轮 sid 更新为正式号, + // 后续状态操作(消息/分页/结果)都落到新 key + sid = currentIdOf(sid); if (activeHandler !== handler) return; // 等待期被终止/切走 → 不再发送启动帧 activeRunIsWorkflow.value = true; sendWorkflowStart(ws, { @@ -996,7 +1051,7 @@ const handleFormSubmit = async ( msg.formError = undefined; session.status = 'executing'; - const sessionId = session.sessionId || getSessionId(); + const sessionId = resolveSessionIdForConnect(session); await runWorkflow(sid, sessionId, msg.form, payload, msg); }; @@ -1151,36 +1206,8 @@ const runChat = async (sid: string, sessionId: string, message: string) => { // 更新会话状态 const session = historyList.value.find((h) => h.id === sid); if (session) session.status = success ? 'completed' : 'failed'; - // 虚拟会话对话后刷新会话列表替换真实条目 - if (session && session.id.startsWith('virtual_')) { - getSessionListV2({ pageNum: 1, pageSize: 10 }).then((sessionRes) => { - const freshList = getSessionData(sessionRes) - .filter((s: any) => s.sessionId) - .map((s: any) => ({ - id: String(s.sessionId), - sessionId: String(s.sessionId), - title: s.sessionName || '未命名会话', - time: s.createdAt?.substring(0, 10) || '', - })); - const match = freshList.find((s: any) => s.sessionId === session.sessionId); - if (match) { - const idx = historyList.value.findIndex((h) => h.id === session.id); - if (idx >= 0) { - const msgs = sessionMessages.value.get(session.id); - sessionMessages.value.delete(session.id); - sessionMessages.value.set(match.id, msgs || []); - historyList.value[idx] = { ...match, status: session.status }; - if (activeHistoryId.value === session.id) { - activeHistoryId.value = match.id; - } - } - loadSessionResults(match.id); - } - }); - } else { - // 非虚拟会话:对话完成/失败后直接刷新会话结果 - loadSessionResults(sid); - } + // 会话在建连时已认领为后端正式号(UUID),对话完成/失败后直接刷新会话结果 + loadSessionResults(sid); delete sendingSessions[sid]; }; @@ -1299,6 +1326,8 @@ const runChat = async (sid: string, sessionId: string, message: string) => { finishChat(false, 'WebSocket 初始化失败,请检查服务地址'); return; } + // 首次建连已认领后端正式号(ensureSocket 就绪前 ack 已处理):本轮 sid 更新为正式号 + sid = currentIdOf(sid); if (activeHandler !== handler) return; // 等待期被终止/切走 → 不再发送启动帧 sendAgentStart(ws, { modelId: chatModelId, question: message }); }; @@ -1330,8 +1359,8 @@ const handleSelectHistory = async (id: string) => { if (session?.status === 'completed' || session?.status === 'failed' || (session?.status === 'executing' && !sendingSessions[session.id])) { session.status = undefined; } - // 真实会话打开即预建连(fire-and-forget);虚拟会话首次提问才建连 - if (session && !session.sessionId.startsWith('virtual_')) { + // 已认领后端正式号的会话打开即预建连(fire-and-forget);未认领的本地临时会话首次提问才建连 + if (session && hasBackendSessionId(session.sessionId)) { void ensureSocket(id, session.sessionId); } }; @@ -1533,8 +1562,8 @@ const handleDeleteHistory = async (id: string) => { } else if (wsState?.sid === id) { closeCurrentSocket(); } - const isVirtual = id.startsWith('virtual_'); - if (!isVirtual) { + // 已认领后端正式号的会话才调后端删除接口;未认领的本地临时会话后端无记录,跳过删除 + if (hasBackendSessionId(id)) { try { await deleteSessionV2(id); } catch { diff --git a/src/views/home/utils/flowDsl.ts b/src/views/home/utils/flowDsl.ts index 5d2e105..bf7d565 100644 --- a/src/views/home/utils/flowDsl.ts +++ b/src/views/home/utils/flowDsl.ts @@ -194,3 +194,26 @@ export function extractWorkflowOutputs(data: any, backendId?: string): WorkflowO if (data.resultUrl) push(data.resultUrl); return outputs; } + +// 从事件携带的文件 URL 数组(flow_complete 的 data.resultFileUrls)直接构建产出列表: +// 不依赖执行详情的异步查询,type 同样按扩展名推断,去重后归一为 { url, name, type, backendId }。 +// 用于完成事件即时渲染产出卡片,删除时 backendId 为空则跳过删除接口(安全降级)。 +export function buildOutputsFromUrls(urls: any, backendId?: string): WorkflowOutput[] { + if (!Array.isArray(urls)) return []; + const seen = new Set(); + const outputs: WorkflowOutput[] = []; + for (const u of urls) { + if (typeof u !== 'string') continue; + const url = u.trim(); + if (!url || seen.has(url)) continue; + seen.add(url); + const name = String(url).split('?')[0].split('/').pop() || '产出文件'; + outputs.push({ + url, + name, + type: guessFileType(url), + backendId: backendId || '', + }); + } + return outputs; +} diff --git a/src/views/home/utils/wsExecute.ts b/src/views/home/utils/wsExecute.ts index 4f7bf90..6945000 100644 --- a/src/views/home/utils/wsExecute.ts +++ b/src/views/home/utils/wsExecute.ts @@ -19,10 +19,13 @@ export interface SessionSocket { } export interface ConnectSessionSocketOptions { - sessionId: string; + // 可选:已认领会话(后端正式号)传回复用;未传(空串)则后端自动创建,从首帧 ack 认领 + sessionId?: string; modelId?: string | number; flowId?: string | number; onMessage: (raw: any) => void; + // 未传 sessionId 时,后端创建会话后首帧 ack 返回的正式会话号(UUID),调用方据此认领本地会话 + onClaim?: (sessionId: string) => void; onClose: (ev: CloseEvent) => void; onError: (ev: Event) => void; } @@ -35,7 +38,10 @@ export interface ConnectSessionSocketOptions { */ export function connectSessionSocket(opts: ConnectSessionSocketOptions): SessionSocket | null { const base = getWsBase(); - const params: Record = { sessionId: opts.sessionId }; + const params: Record = {}; + // 已认领会话传号复用;未传号则由后端自动创建会话,首帧 ack 返回正式号 + const sidForConnect = opts.sessionId; + if (sidForConnect) params.sessionId = sidForConnect; if (opts.flowId != null) params.flowId = String(opts.flowId); if (opts.modelId != null) params.modelId = String(opts.modelId); const token = Session.get('token'); @@ -61,16 +67,42 @@ export function connectSessionSocket(opts: ConnectSessionSocketOptions): Session readyReject = reject; }); + // 未传会话号:等后端首帧 ack(data.sessionId)认领后才算就绪,避免业务帧先于会话号发出 + const wantsClaim = !opts.sessionId; + let claimed = false; // 打开前是否已就绪:用于区分「未连上就关闭」与「正常关闭」 let opened = false; + // 兜底:ack 迟迟未到(异常)时超时放行,降级用原本地 id 继续,避免永久卡在建连 + let claimTimer: number | null = null; ws.onopen = () => { // 长连接:握手成功仅标记就绪,不发送任何启动帧 opened = true; - readyResolve(ws); + if (!wantsClaim) { + readyResolve(ws); + } else { + claimTimer = window.setTimeout(() => { + claimed = true; + readyResolve(ws); + }, 5000); + } + }; + ws.onmessage = (ev) => { + // 首次建连:首帧 ack 携带后端正式会话号 → 认领后放行业务帧 + if (wantsClaim && !claimed) { + const ackId = parseAckSessionId(ev.data); + if (ackId) { + claimed = true; + if (claimTimer != null) window.clearTimeout(claimTimer); + opts.onClaim?.(ackId); + readyResolve(ws); + return; // ack 帧仅用于认领,不交给业务 handler + } + } + opts.onMessage(ev.data); }; - ws.onmessage = (ev) => opts.onMessage(ev.data); ws.onclose = (ev) => { + if (claimTimer != null) window.clearTimeout(claimTimer); if (!opened) readyReject(ev); opts.onClose(ev); }; @@ -79,6 +111,22 @@ export function connectSessionSocket(opts: ConnectSessionSocketOptions): Session return { ws, ready }; } +// 解析建连 ack 帧:{ type:'ack', data:{ sessionId } } → 返回 sessionId;非 ack / 无号返回空串 +function parseAckSessionId(raw: any): string { + let data: any = raw; + if (typeof raw === 'string') { + try { + data = JSON.parse(raw); + } catch { + return ''; + } + } + if (!data || typeof data !== 'object') return ''; + if (data.type !== 'ack') return ''; + const sid = data?.data?.sessionId; + return typeof sid === 'string' && sid ? sid : ''; +} + // ===== 启动帧 ===== /** 普通对话:发送提问(type=agent),后端据此开始推流 */ diff --git a/src/views/home/utils/wsMessage.ts b/src/views/home/utils/wsMessage.ts index f862105..f4bcf06 100644 --- a/src/views/home/utils/wsMessage.ts +++ b/src/views/home/utils/wsMessage.ts @@ -30,6 +30,7 @@ export const WsEventType = { ReasoningChunk: 'reasoning_chunk', // 思考内容增量(逐 chunk) NodeStart: 'node_start', // 工作流节点开始(进度推进) NodeComplete: 'node_complete', // 工作流节点完成(进度推进) + FlowComplete: 'flow_complete', // 工作流执行完成(后端完成信号,日志末尾事件) Error: 'error', // 出错 } as const; @@ -108,11 +109,11 @@ export function getToolResultText(msg: WsStreamMessage): string { return ''; } -/** error → 错误文案(error 字段为详细原因优先;其次 message;再 data.message / data.error / data.msg) */ +/** error → 错误文案(message 优先展示简洁业务提示;error 详细原因仅在 message 缺失时兜底) */ export function getErrorText(msg: WsStreamMessage): string { - // 详细失败原因(含节点名/具体错误),优先展示 - if (typeof msg?.error === 'string' && msg.error) return msg.error; + // 只显示 message 的消息(用户确认),error 详细原因不再作为界面展示内容 if (typeof msg?.message === 'string' && msg.message) return msg.message; + if (typeof msg?.error === 'string' && msg.error) return msg.error; const d = msg?.data; if (typeof d === 'string') return d; if (d && typeof d === 'object') {