Merge branch 'master-nj' into 'master'

Master nj

See merge request knowledge-base-bj/deerflow-code!23
This commit is contained in:
董宏凯 committed 2026-08-14 05:01:17 +00:00
commit 96bfdacee5
6 files changed
+1912 -20

No files matched your search

File diff suppressed because it is too large. Load diff
@@ -1229,7 +1229,6 @@ export function InputBox({
</PromptInputTools> </PromptInputTools>
<PromptInputTools className={cn("gap-1.5", compact && "gap-2")}> <PromptInputTools className={cn("gap-1.5", compact && "gap-2")}>
{!frontendRagActive && agentSelector && ( {!frontendRagActive && agentSelector && (
{!frontendRagActive && knowledgeSelector}
<AgentSelector <AgentSelector
selectedAgentId={agentSelector.selectedAgentId} selectedAgentId={agentSelector.selectedAgentId}
onAgentChange={agentSelector.onAgentChange} onAgentChange={agentSelector.onAgentChange}
@@ -1237,6 +1236,7 @@ export function InputBox({
compact={compact} compact={compact}
/> />
)} )}
{!frontendRagActive && knowledgeSelector}
{!frontendRagActive && promptPrefix && ( {!frontendRagActive && promptPrefix && (
<Tooltip <Tooltip
content={ content={
@@ -1,5 +1,6 @@
import type { Message } from '@langchain/langgraph-sdk'; import type { Message } from '@langchain/langgraph-sdk';
import { import {
BookMarkedIcon,
ChevronDownIcon, ChevronDownIcon,
ClockIcon, ClockIcon,
ExternalLinkIcon, ExternalLinkIcon,
@@ -42,6 +43,11 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from '@/components/ui/dialog'; } from '@/components/ui/dialog';
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from '@/components/ui/collapsible';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
import { import {
UploadedFilePreviewDialog, UploadedFilePreviewDialog,
@@ -788,6 +794,12 @@ function MessageContent_({
/> />
)} )}
</div> </div>
{(isSingleRoundRag || isMultiRoundRag) && displaySources.length > 0 && (
<FrontendRagSourcesPanel
sources={displaySources}
onSelect={setSelectedCitation}
/>
)}
<MessageTokenUsage <MessageTokenUsage
enabled={tokenUsageEnabled} enabled={tokenUsageEnabled}
isLoading={isLoading} isLoading={isLoading}
@@ -809,6 +821,76 @@ function MessageContent_({
); );
} }
/**
* 单轮/多轮 RAG 专属的「引用来源」折叠面板。
*
* 参考 GeneralQA 的引用来源区:默认折叠,列出全部参考资料,点击任一条打开详情
* 弹窗(复用 SingleRoundRagCitationDialog)。这样即使模型正文里没有 `[1]` 编号,
* 用户也能看到本次回答依据的资料。
*/
function FrontendRagSourcesPanel({
sources,
onSelect,
}: {
sources: RuntimeDisplaySource[];
onSelect: (source: RuntimeDisplaySource) => void;
}) {
const [open, setOpen] = useState(false);
if (sources.length === 0) return null;
return (
<Collapsible open={open} onOpenChange={setOpen} className="mt-3 w-full">
<CollapsibleTrigger asChild>
<button
type="button"
className="text-muted-foreground hover:text-foreground inline-flex items-center gap-1.5 text-xs transition-colors"
>
<BookMarkedIcon className="size-3.5" />
<span className="font-medium">引用来源</span>
<span className="text-muted-foreground/70">({sources.length})</span>
<ChevronDownIcon
className={cn(
'size-3.5 transition-transform duration-200',
!open && '-rotate-90',
)}
/>
</button>
</CollapsibleTrigger>
<CollapsibleContent className="mt-2 grid gap-2">
{sources.map((source, idx) => (
<button
key={source.id ?? `${source.title}-${idx}`}
type="button"
onClick={() => onSelect(source)}
className="bg-card hover:bg-accent block w-full cursor-pointer rounded-lg border p-3 text-left transition-colors"
>
<div className="flex items-center gap-2">
<span className="bg-primary/10 text-primary flex size-5 shrink-0 items-center justify-center rounded-full text-xs font-semibold">
{source.index}
</span>
<span
className="text-foreground line-clamp-1 min-w-0 flex-1 text-sm font-medium"
title={source.title}
>
{source.title}
</span>
{source.type && (
<span className="bg-muted text-muted-foreground shrink-0 rounded px-1.5 py-0.5 text-xs">
{source.type}
</span>
)}
</div>
{source.snippet && (
<p className="text-muted-foreground mt-1.5 line-clamp-2 text-xs leading-relaxed">
{source.snippet}
</p>
)}
</button>
))}
</CollapsibleContent>
</Collapsible>
);
}
function SingleRoundRagCitationDialog({ function SingleRoundRagCitationDialog({
source, source,
onOpenChange, onOpenChange,
@@ -30,6 +30,7 @@ import { useSkills } from "@/core/skills/hooks";
import { import {
buildSkillDisplayMap, buildSkillDisplayMap,
collectRuntimeDisplaySources, collectRuntimeDisplaySources,
type RuntimeDisplaySource,
} from "@/core/skill-display/runtime"; } from "@/core/skill-display/runtime";
import type { Subtask } from "@/core/tasks"; import type { Subtask } from "@/core/tasks";
import { useUpdateSubtask } from "@/core/tasks/context"; import { useUpdateSubtask } from "@/core/tasks/context";
@@ -259,6 +260,53 @@ export function MessageList({
// frontend tool-message scan below comes up empty (e.g. after a reload). // frontend tool-message scan below comes up empty (e.g. after a reload).
const referenceBatches = useContext(ReferenceBatchesContext); const referenceBatches = useContext(ReferenceBatchesContext);
// 单轮/多轮 RAG 的参考资料存放在一条 hide_from_ui 的 tool 消息里(紧挨在
// assistant 消息之前)。allMessages 会把 hide_from_ui 消息过滤掉,导致通用
// 的 collectRuntimeDisplaySources 扫描不到它们。这里直接在原始消息流里按
// assistant 消息提取其对应的参考资料,保证 `[1]` 等编号能解析成可点击引用。
const frontendRagSourcesByAiId = useMemo(() => {
const map = new Map<string, RuntimeDisplaySource[]>();
const raw = thread.messages;
for (let i = 0; i < raw.length; i++) {
const msg = raw[i];
if (!msg || msg.type !== "ai" || !msg.id) continue;
const kwargs = msg.additional_kwargs;
if (!kwargs || (kwargs.single_round_rag !== true && kwargs.multi_round_rag !== true)) {
continue;
}
let sources: RuntimeDisplaySource[] = [];
for (let j = i - 1; j >= 0; j--) {
const prev = raw[j];
if (!prev) break;
// 前一条 human/ai 消息即为本轮边界,跨边界即停止。
if (prev.type === "human" || prev.type === "ai") break;
if (prev.type !== "tool") continue;
const prevKwargs = prev.additional_kwargs;
if (
!prevKwargs ||
(prevKwargs.single_round_rag !== true && prevKwargs.multi_round_rag !== true)
) {
continue;
}
const results = extractFrontendRagResults(prev.content);
if (results && results.length > 0) {
const toolName = String(
(prev as { name?: unknown }).name ??
(prevKwargs.multi_round_rag === true
? "multi_round_rag_references"
: "knowledge_base_search"),
);
sources = results.map((item, index) =>
toRuntimeDisplaySource(item, index + 1, toolName),
);
}
break;
}
map.set(msg.id, sources);
}
return map;
}, [thread.messages]);
// Track which clarification messages are still awaiting an answer. // Track which clarification messages are still awaiting an answer.
// Pending = clarification tool messages that appear after the last human message // Pending = clarification tool messages that appear after the last human message
// (no user reply has been sent for them yet). // (no user reply has been sent for them yet).
@@ -392,14 +440,21 @@ export function MessageList({
(batch) => batch.assistant_message_id === msg.id, (batch) => batch.assistant_message_id === msg.id,
)?.sources ?? [] )?.sources ?? []
: []; : [];
// 单轮/多轮 RAG 的参考资料来自隐藏的 tool 消息,优先于通用扫描。
const frontendRagSources =
isAssistantMessage && msg.id
? frontendRagSourcesByAiId.get(msg.id) ?? []
: [];
const displaySources = isAssistantMessage const displaySources = isAssistantMessage
? batchSources.length > 0 ? batchSources.length > 0
? batchSources ? batchSources
: collectRuntimeDisplaySources(messages, skillDisplayMap, { : frontendRagSources.length > 0
afterMessageId: findPreviousHumanMessageId(messages, msg.id), ? frontendRagSources
beforeMessageId: msg.id, : collectRuntimeDisplaySources(messages, skillDisplayMap, {
includeBeforeMessage: true, afterMessageId: findPreviousHumanMessageId(messages, msg.id),
}) beforeMessageId: msg.id,
includeBeforeMessage: true,
})
: []; : [];
return ( return (
<MessageListItem <MessageListItem
@@ -728,6 +783,46 @@ function findPreviousHumanMessageId(
return undefined; return undefined;
} }
/** 从单轮/多轮 RAG 的 tool 消息体里取出 `content.results`。 */
function extractFrontendRagResults(content: unknown): unknown[] | null {
if (content && typeof content === "object" && !Array.isArray(content)) {
const results = (content as { results?: unknown }).results;
if (Array.isArray(results)) return results;
}
return null;
}
/** 把单轮/多轮 RAG 的一条参考资料归一化为展示用的 RuntimeDisplaySource。 */
function toRuntimeDisplaySource(
item: unknown,
index: number,
skillName: string,
): RuntimeDisplaySource {
const record =
item && typeof item === "object" && !Array.isArray(item)
? (item as Record<string, unknown>)
: {};
const raw =
(record.raw as Record<string, unknown> | undefined) ?? record;
const content = String(record.content ?? record.snippet ?? "");
return {
id: typeof record.id === "string" && record.id ? record.id : undefined,
title: String(record.title ?? `参考资料 ${index}`),
type: String(record.type ?? "result"),
snippet: String(record.snippet ?? content),
content,
source:
typeof record.source === "string" && record.source ? record.source : undefined,
author: undefined,
time: typeof record.time === "string" && record.time ? record.time : undefined,
url: typeof record.url === "string" && record.url ? record.url : undefined,
raw,
index,
skillName,
mode: "citation",
};
}
function getSetupAgentApprovalPayload( function getSetupAgentApprovalPayload(
message: unknown, message: unknown,
): SetupAgentApprovalPayload | null { ): SetupAgentApprovalPayload | null {
+1 -5
View File
@@ -815,11 +815,7 @@ export default function ChatPage() {
: "ready" : "ready"
} }
context={settings.context} context={settings.context}
extraModels={ extraModels={SINGLE_ROUND_RAG_MODELS}
singleRoundRagSelected
? SINGLE_ROUND_RAG_MODELS
: undefined
}
disabled={ disabled={
env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" || env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" ||
isUploading || isUploading ||
+3 -9
View File
@@ -364,8 +364,7 @@ auth_login:
# 点击后在新标签页打开下面的 url。enabled 关掉则不显示该入口。 # 点击后在新标签页打开下面的 url。enabled 关掉则不显示该入口。
database: database:
backend: postgres backend: sqlite
postgres_url: $POSTGRES_DATABASE_URL
sqlite_dir: .deer-flow/data sqlite_dir: .deer-flow/data
pool_size: 20 # 基础连接池(postgres/mysql 生效;sqlite 忽略) pool_size: 20 # 基础连接池(postgres/mysql 生效;sqlite 忽略)
max_overflow: 20 # 高峰期可临时超出 pool_size 的额外连接数,真实上限 = pool_size + max_overflow max_overflow: 20 # 高峰期可临时超出 pool_size 的额外连接数,真实上限 = pool_size + max_overflow
@@ -374,13 +373,8 @@ database:
echo_sql: false echo_sql: false
checkpointer: checkpointer:
type: postgres type: sqlite
connection_string: $POSTGRES_CHECKPOINT_URL connection_string: .deer-flow/data/deerflow.db
use_postgres: true
# PostgreSQL 优先;SQLite 仅作历史记录读取兜底,确认稳定后可移除。
dual_read_enabled: true
dual_read_sqlite_paths:
- .deer-flow/data/deerflow.db
# 记忆子系统:跨会话持久化用户画像与 agent 私有记忆。 # 记忆子系统:跨会话持久化用户画像与 agent 私有记忆。
# 三层覆盖优先级(高->低): # 三层覆盖优先级(高->低):