diff --git a/frontend/src/sdk-test/SdkTestPanel.vue b/frontend/src/sdk-test/SdkTestPanel.vue index 24f62c2..0b12b6d 100644 --- a/frontend/src/sdk-test/SdkTestPanel.vue +++ b/frontend/src/sdk-test/SdkTestPanel.vue @@ -288,6 +288,7 @@ import '@tdesign-vue-next/chat/es/style/index.css' import { MessagePlugin } from 'tdesign-vue-next' import { readSSEStream } from '@/utils/sse' import { renderMarkdown } from '@/utils/markdown' +import { toast } from '@/utils/toast' import { toChatData, type ChatMessage } from '@/utils/chatAdapter' // ==================== 常量 ==================== @@ -673,17 +674,21 @@ async function sendDemo(val?: string): Promise { demoMessages.value.push({ id: genId(), role: 'user', content: text, streaming: false, time: fmtTime() }) const assistantMsg: ChatMessage = { id: genId(), role: 'assistant', content: '', streaming: true, time: fmtTime(), sources: [] } demoMessages.value.push(assistantMsg) - await scrollDemoBottom() + const cid = demoChatId.value + const requestMode = demoMode.value const opts = { enableRag: demoRag.value, rewriteStrategy: demoRag.value ? demoStrategy.value : undefined } - const url = buildDemoUrl(demoMode.value === 'sync' ? 'chat' : 'chat/stream', text, demoChatId.value, opts) + // 提前构建 URL,固定本轮角色、账号、域名和检索配置。 + const url = buildDemoUrl(requestMode === 'sync' ? 'chat' : 'chat/stream', text, cid, opts) + const sourcesUrl = opts.enableRag ? buildDemoUrl('chat/sources', text, cid, opts) : '' const headers = sdkAuthHeaders() + await scrollDemoBottom() try { if (abortController) abortController.abort() abortController = new AbortController() - if (demoMode.value === 'sync') { + if (requestMode === 'sync') { const res = await fetch(url, { headers, signal: abortController.signal }) if (!res.ok) throw new Error('HTTP ' + res.status) assistantMsg.content = await res.text() @@ -701,15 +706,19 @@ async function sendDemo(val?: string): Promise { ) } - // RAG 模式下拉取引用来源 - if (demoRag.value) { - try { - const sres = await fetch(buildDemoUrl('chat/sources', text, demoChatId.value, opts), { headers }) + // 引用来源后台补齐,正文完成后立即结束发送状态。 + if (opts.enableRag) { + void fetch(sourcesUrl, { headers }).then(async sres => { const json = await sres.json() - if (json && json.success) assistantMsg.sources = json.data || [] - } catch { - /* 来源获取失败不影响主回答 */ - } + if (!sres.ok || !json?.success) { + throw new Error(json?.message || (sres.ok ? '引用来源获取失败' : 'HTTP ' + sres.status)) + } + if (demoChatId.value !== cid || !demoMessages.value.some(msg => msg.id === assistantMsg.id)) return + assistantMsg.sources = json.data || [] + demoMessages.value = [...demoMessages.value] + }).catch((e: any) => { + toast('引用来源获取失败: ' + (e.message || e), 'warning') + }) } } catch (e: any) { if (e.name === 'AbortError') { diff --git a/frontend/src/views/ChatPanel.vue b/frontend/src/views/ChatPanel.vue index 63dd2ba..c61c80c 100644 --- a/frontend/src/views/ChatPanel.vue +++ b/frontend/src/views/ChatPanel.vue @@ -491,6 +491,19 @@ async function send(): Promise { isSending.value = true currentSuggestions.value = [] // 清空建议 + const cid = chatId.value || ('web_' + Date.now()) + chatId.value = cid + const requestMode = mode.value + // 固定本轮参数,后台引用请求不读取后续切换的角色或 RAG 配置。 + const imageUrls = attachments.filter(a => a.type === 'image').map(a => a.url) + const chatOptions: ChatOptions = { + roleId: currentRoleId(), + enableRag: isRagMode.value, + rewriteStrategy: isRagMode.value ? ragStrategy.value : undefined, + categoryIds: [...selectedCategoryIds.value], + imageUrls: imageUrls.length ? imageUrls : undefined, + } + // 追加用户消息(含附件) messages.value.push({ id: generateMsgId(), role: 'user', content: text, @@ -505,32 +518,19 @@ async function send(): Promise { messages.value.push(assistantMsg) await scrollToBottom() - const cid = chatId.value || ('web_' + Date.now()) - chatId.value = cid - try { // 取消上一个 SSE 请求并创建新的 AbortController if (sseAbortController) { sseAbortController.abort() } sseAbortController = new AbortController() const signal = sseAbortController.signal - // 统一对话请求参数(RAG 与普通对话共用同一接口,由 enableRag 参数区分) - const imageUrls = attachments.filter(a => a.type === 'image').map(a => a.url) - const chatOptions: ChatOptions = { - roleId: currentRoleId(), - enableRag: isRagMode.value, - rewriteStrategy: isRagMode.value ? ragStrategy.value : undefined, - categoryIds: selectedCategoryIds.value, - imageUrls: imageUrls.length ? imageUrls : undefined, - } - - if (mode.value === 'sync') { + if (requestMode === 'sync') { // 同步调用 assistantMsg.content = await chatSync(text, cid, chatOptions) } else { // SSE 流式 const url = chatSSEUrl(text, cid, chatOptions) - if (isRagMode.value) { + if (chatOptions.enableRag) { await readSSEStreamWithEvents(url, { onMessage: async (chunk: string) => { assistantMsg.content += chunk @@ -562,12 +562,16 @@ async function send(): Promise { } } - // RAG 模式下,拉取引用来源 - if (isRagMode.value) { - try { - const sj = await ragSources(text, cid, chatOptions) - if (sj && sj.success) assistantMsg.sources = sj.data || [] - } catch (_) { /* 来源获取失败不影响主回答 */ } + // 引用来源后台补齐,正文完成后立即结束发送状态。 + if (chatOptions.enableRag) { + void ragSources(text, cid, chatOptions).then(sj => { + if (!sj?.success) throw new Error(sj?.message || '引用来源获取失败') + if (chatId.value !== cid || !messages.value.some(msg => msg.id === assistantMsg.id)) return + assistantMsg.sources = sj.data || [] + messages.value = [...messages.value] + }).catch((e: any) => { + toast('引用来源获取失败: ' + (e.message || e), 'warning') + }) } } catch (e: any) { // AbortError 不是真正的错误,不显示错误信息