daimhebing1

This commit is contained in:
fengxiangdebaozi committed 2026-08-14 21:25:29 +08:00
1 parent 067f602732
commit 5c586652af
2 files changed
+131 -54

No files matched your search

@@ -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") {
@@ -5,7 +5,7 @@ import { readRuntimeEnv } from "@/env";
const STORAGE_PREFIX = "workspace.single-round-rag.";
type KnowledgeBaseChunk = {
type KnowledgeBaseChunk = Record<string, unknown> & {
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<string, unknown>,
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<string, unknown>;
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<string, unknown>, 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();
}
}