From 5c586652afb3fc0bf581bc10c8fc03dd8ea6135f Mon Sep 17 00:00:00 2001 From: fengxiangdebaozi <10845145+fengxiangdebaozi@user.noreply.gitee.com> Date: Fri, 14 Aug 2026 21:25:29 +0800 Subject: [PATCH] daimhebing1 --- .../components/workspace/chats/chat-box.tsx | 32 +++- .../src/core/threads/use-single-round-rag.ts | 153 ++++++++++++------ 2 files changed, 131 insertions(+), 54 deletions(-) diff --git a/frontend-web/src/components/workspace/chats/chat-box.tsx b/frontend-web/src/components/workspace/chats/chat-box.tsx index f8021559..c185cb43 100644 --- a/frontend-web/src/components/workspace/chats/chat-box.tsx +++ b/frontend-web/src/components/workspace/chats/chat-box.tsx @@ -777,11 +777,10 @@ function buildTurnReferenceBatches( continue; } const nextHumanIndex = findNextHumanIndex(messages, index + 1); - if (nextHumanIndex === -1 && isLoading) { - continue; - } const endIndex = nextHumanIndex === -1 ? messages.length : nextHumanIndex; - const assistant = findLastAssistantAnswer(messages, index + 1, endIndex); + const assistant = + findLastAssistantAnswer(messages, index + 1, endIndex) ?? + findFrontendRagAssistant(messages, index + 1, endIndex); if (!assistant?.id) { continue; } @@ -791,6 +790,14 @@ function buildTurnReferenceBatches( nextHumanIndex === -1 ? undefined : messages[nextHumanIndex]?.id, includeBeforeMessage: false, }).filter((source) => source.mode !== "none"); + // GeneralQA's single-RAG stream sends `docs` before the answer. Make + // those cards available immediately, while keeping normal streaming turns + // on their existing "wait for an answer" behavior. + const isFrontendRagTurn = + isSingleRoundRagMessage(assistant) || isMultiRoundRagMessage(assistant); + if (nextHumanIndex === -1 && isLoading && !isFrontendRagTurn) { + continue; + } if (sources.length > 0) { batches.push({ id: assistant.id, @@ -829,6 +836,23 @@ function findLastAssistantAnswer( return null; } +function findFrontendRagAssistant( + messages: Message[], + start: number, + end: number, +): Message | null { + for (let index = end - 1; index >= start; index -= 1) { + const message = messages[index]; + if ( + message?.type === "ai" && + (isSingleRoundRagMessage(message) || isMultiRoundRagMessage(message)) + ) { + return message; + } + } + return null; +} + function hasAssistantAnswerContent(message: Message): boolean { const content = message.content; if (typeof content === "string") { diff --git a/frontend-web/src/core/threads/use-single-round-rag.ts b/frontend-web/src/core/threads/use-single-round-rag.ts index 3d3e925b..bd6ee3e5 100644 --- a/frontend-web/src/core/threads/use-single-round-rag.ts +++ b/frontend-web/src/core/threads/use-single-round-rag.ts @@ -5,7 +5,7 @@ import { readRuntimeEnv } from "@/env"; const STORAGE_PREFIX = "workspace.single-round-rag."; -type KnowledgeBaseChunk = { +type KnowledgeBaseChunk = Record & { answer?: unknown; docs?: unknown; }; @@ -168,63 +168,114 @@ function parseKnowledgeBaseChunk(jsonText: string): KnowledgeBaseChunk { } } +function readString(...values: unknown[]): string | undefined { + for (const value of values) { + if (typeof value === "string" && value.trim()) return value; + } + return undefined; +} + +function docsFallbackContent(docs: unknown): string | undefined { + const candidates = Array.isArray(docs) ? docs : [docs]; + return readString(...candidates); +} + +function buildKnowledgeDetailUrl(recUuid: string | undefined): string | undefined { + const knowledgeUrl = readRuntimeEnv("VITE_AIPI_KNOWLEDGE_URL")?.replace(/\/$/, ""); + if (!knowledgeUrl || !recUuid) return undefined; + return `${knowledgeUrl}/#/knowledge/mid-detail?recUuid=${encodeURIComponent(recUuid)}`; +} + +function toRagSource( + record: Record, + index: number, + fallbackContent?: string, +): RagSource { + const title = + readString(record.m_title, record.title, record.source, record.source1) ?? + `参考资料 ${index + 1}`; + const content = + readString( + record.m_content, + // Some deployed responses spell this field incorrectly; keep it as a + // read-only compatibility alias rather than dropping the document. + record.m_conent, + record.content, + record.snippet, + fallbackContent, + record.url, + ) ?? ""; + const source = readString(record.source1, record.source, record.m_source); + const rawUrl = readString(record.url, record.link, record.href); + const recUuid = readString(record.recUuid, record.rec_uuid, record.uuid, record.id); + return { + id: recUuid, + title, + type: readString(record.type, record.source1) ?? "知识库", + snippet: content, + content, + source, + time: readString(record.m_publish, record.m_publicsh, record.time), + // GeneralQA uses recUuid to open the knowledge-detail page. Prefer that + // stable internal record link, then fall back to a backend-provided URL. + url: + buildKnowledgeDetailUrl(recUuid) ?? + (rawUrl && /^https?:\/\//i.test(rawUrl) ? rawUrl : undefined), + raw: record, + }; +} + /** - * Mirrors GeneralQA's `mapDocsToSources` behavior: `docs` may be an array, - * a single document, or a plain string. The previous adapter only accepted - * objects and consequently dropped the first stream payload whenever the - * knowledge-base service returned a string document. + * A single-RAG stream is a concatenation of source envelopes, for example: + * `{docs:["..."], recUuid:"...", m_title:"...", m_content:"..."}`. + * The root-level metadata is the source; `docs` merely marks the envelope and + * supplies a fallback body. This mirrors GeneralQA's per-chunk processing. */ -function normalizeSources(value: KnowledgeBaseChunk["docs"]): RagSource[] { - const docs = Array.isArray(value) ? value : value ? [value] : []; +function normalizeSources(chunk: KnowledgeBaseChunk): RagSource[] { + const hasRootMetadata = [ + chunk.recUuid, + chunk.rec_uuid, + chunk.m_title, + chunk.m_content, + chunk.m_conent, + chunk.source1, + chunk.m_publish, + chunk.m_publicsh, + ].some((value) => typeof value === "string" && value.trim()); + if (hasRootMetadata) { + return [toRagSource(chunk, 0, docsFallbackContent(chunk.docs))]; + } + + const docs = Array.isArray(chunk.docs) ? chunk.docs : chunk.docs ? [chunk.docs] : []; return docs.flatMap((item, index): RagSource[] => { if (typeof item === "string") { - return [ - { - title: `参考资料 ${index + 1}`, - type: "知识库", - snippet: item, - content: item, - raw: { content: item }, - }, - ]; + return [toRagSource({}, index, item)]; } if (!item || typeof item !== "object") return []; - - const record = item as Record; - const title = String( - record.title ?? record.m_title ?? record.source ?? `参考资料 ${index + 1}`, - ); - const content = String( - record.content ?? record.m_content ?? record.snippet ?? record.url ?? "", - ); - const source = record.source ?? record.source1 ?? record.title ?? record.m_title; - const rawUrl = record.url ?? record.link ?? record.href; - return [ - { - id: - String(record.recUuid ?? record.rec_uuid ?? record.uuid ?? record.id ?? "") || - undefined, - title, - type: String(record.source1 ?? record.type ?? "知识库"), - snippet: content, - content, - source: typeof source === "string" ? source : undefined, - time: - typeof record.m_publish === "string" - ? record.m_publish - : typeof record.time === "string" - ? record.time - : undefined, - url: - typeof rawUrl === "string" && /^https?:\/\//i.test(rawUrl) - ? rawUrl - : undefined, - raw: record, - }, - ]; + return [toRagSource(item as Record, index)]; }); } +function mergeSources(current: RagSource[], incoming: RagSource[]): RagSource[] { + const result = [...current]; + const keys = new Set( + current.map((source) => + source.id + ? `id:${source.id}` + : `content:${source.title}\u0000${source.content}\u0000${source.source ?? ""}`, + ), + ); + for (const source of incoming) { + const key = source.id + ? `id:${source.id}` + : `content:${source.title}\u0000${source.content}\u0000${source.source ?? ""}`; + if (keys.has(key)) continue; + keys.add(key); + result.push(source); + } + return result; +} + function createId(prefix: string) { return `${prefix}-${crypto.randomUUID()}`; } @@ -424,7 +475,9 @@ export function useSingleRoundRagMessages(threadId: string) { for (const jsonText of extracted.objects) { const chunk = parseKnowledgeBaseChunk(jsonText); if (typeof chunk.answer === "string") answerChunks.push({ answer: chunk.answer }); - if (chunk.docs !== undefined) sources = normalizeSources(chunk.docs); + if (chunk.docs !== undefined) { + sources = mergeSources(sources, normalizeSources(chunk)); + } refreshAssistant(); } }