Merge branch 'master' of http://192.168.0.200/knowledge-base-bj/deerflow-server into ji
# Conflicts: # frontend-web/src/components/workspace/messages/message-list-item.tsx # frontend-web/src/components/workspace/messages/message-list.tsx # offline-backend-20260512/backend/packages/harness/deerflow/agents/lead_agent/agent.py # offline-backend-20260512/backend/packages/harness/deerflow/agents/lead_agent/prompt.py
This commit is contained in:
commit
6fd6d564eb
54 files changed
+9982
-4557
No files matched your search
@@ -82,6 +82,7 @@ export function MessageListItem({
|
||||
threadId,
|
||||
tokenUsageEnabled = false,
|
||||
displaySources = [],
|
||||
extraRehypePlugins,
|
||||
}: {
|
||||
className?: string;
|
||||
message: Message;
|
||||
@@ -89,6 +90,7 @@ export function MessageListItem({
|
||||
threadId: string;
|
||||
tokenUsageEnabled?: boolean;
|
||||
displaySources?: RuntimeDisplaySource[];
|
||||
extraRehypePlugins?: ReturnType<typeof useRehypeSplitWordsIntoSpans>;
|
||||
}) {
|
||||
const isHuman = message.type === "human";
|
||||
const messageMarkdown =
|
||||
@@ -107,6 +109,7 @@ export function MessageListItem({
|
||||
threadId={threadId}
|
||||
tokenUsageEnabled={tokenUsageEnabled}
|
||||
displaySources={displaySources}
|
||||
extraRehypePlugins={extraRehypePlugins}
|
||||
/>
|
||||
{!isLoading && (
|
||||
<MessageToolbar
|
||||
@@ -168,6 +171,7 @@ function MessageContent_({
|
||||
threadId,
|
||||
tokenUsageEnabled = false,
|
||||
displaySources = [],
|
||||
extraRehypePlugins,
|
||||
}: {
|
||||
className?: string;
|
||||
message: Message;
|
||||
@@ -175,8 +179,13 @@ function MessageContent_({
|
||||
threadId: string;
|
||||
tokenUsageEnabled?: boolean;
|
||||
displaySources?: RuntimeDisplaySource[];
|
||||
extraRehypePlugins?: ReturnType<typeof useRehypeSplitWordsIntoSpans>;
|
||||
}) {
|
||||
const rehypePlugins = useRehypeSplitWordsIntoSpans(isLoading);
|
||||
const baseRehypePlugins = useRehypeSplitWordsIntoSpans(isLoading);
|
||||
const rehypePlugins = useMemo(
|
||||
() => (extraRehypePlugins?.length ? [...baseRehypePlugins, ...extraRehypePlugins] : baseRehypePlugins),
|
||||
[baseRehypePlugins, extraRehypePlugins],
|
||||
);
|
||||
const isHuman = message.type === "human";
|
||||
const resolveArtifact = useArtifactResolver(threadId);
|
||||
const components = useMemo(
|
||||
@@ -184,13 +193,13 @@ function MessageContent_({
|
||||
img: (props: ImgHTMLAttributes<HTMLImageElement>) => (
|
||||
<MessageImage {...props} threadId={threadId} maxWidth="90%" />
|
||||
),
|
||||
a: ({ href, ...props }: AnchorHTMLAttributes<HTMLAnchorElement>) => {
|
||||
if (href?.startsWith("#reference-")) {
|
||||
return <CitationLink href={href} {...props} />;
|
||||
}
|
||||
if (typeof props.children === "string" && /^citation:/i.test(props.children)) {
|
||||
return <CitationLink href={href} {...props} />;
|
||||
}
|
||||
a: ({ href, ...props }: AnchorHTMLAttributes<HTMLAnchorElement>) => {
|
||||
if (href?.startsWith("#reference-")) {
|
||||
return <CitationLink href={href} {...props} />;
|
||||
}
|
||||
if (typeof props.children === "string" && /^citation:/i.test(props.children)) {
|
||||
return <CitationLink href={href} {...props} />;
|
||||
}
|
||||
if (href?.startsWith("/mnt/")) {
|
||||
const url = resolveArtifact(href);
|
||||
return (
|
||||
@@ -291,21 +300,21 @@ function MessageContent_({
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AIElementMessageContent className={className}>
|
||||
{filesList}
|
||||
<div data-reference-batch-id={message.id} className="contents">
|
||||
<AssistantDisplayContent
|
||||
content={contentToDisplay}
|
||||
displaySources={displaySources}
|
||||
isLoading={isLoading}
|
||||
rehypePlugins={[...rehypePlugins, [rehypeKatex, { output: "html" }]]}
|
||||
components={components}
|
||||
/>
|
||||
</div>
|
||||
<MessageTokenUsage
|
||||
enabled={tokenUsageEnabled}
|
||||
isLoading={isLoading}
|
||||
return (
|
||||
<AIElementMessageContent className={className}>
|
||||
{filesList}
|
||||
<div data-reference-batch-id={message.id} className="contents">
|
||||
<AssistantDisplayContent
|
||||
content={contentToDisplay}
|
||||
displaySources={displaySources}
|
||||
isLoading={isLoading}
|
||||
rehypePlugins={[...rehypePlugins, [rehypeKatex, { output: "html" }]]}
|
||||
components={components}
|
||||
/>
|
||||
</div>
|
||||
<MessageTokenUsage
|
||||
enabled={tokenUsageEnabled}
|
||||
isLoading={isLoading}
|
||||
message={message}
|
||||
/>
|
||||
</AIElementMessageContent>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Message } from "@langchain/langgraph-sdk";
|
||||
import type { BaseStream } from "@langchain/langgraph-sdk/react";
|
||||
import type { Message } from "@langchain/langgraph-sdk";
|
||||
import type { BaseStream } from "@langchain/langgraph-sdk/react";
|
||||
import { CheckIcon, SendIcon, XIcon } from "lucide-react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
} from "@/components/ai-elements/conversation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import {
|
||||
extractContentFromMessage,
|
||||
extractPresentFilesFromMessage,
|
||||
@@ -20,14 +20,14 @@ import {
|
||||
hasReasoning,
|
||||
hasToolCalls,
|
||||
} from "@/core/messages/utils";
|
||||
import { useRehypeSplitWordsIntoSpans } from "@/core/rehype";
|
||||
import { isThreadRagModeLocked, useLocalSettings } from "@/core/settings";
|
||||
import { useSkills } from "@/core/skills/hooks";
|
||||
import {
|
||||
buildSkillDisplayMap,
|
||||
collectRuntimeDisplaySources,
|
||||
} from "@/core/skill-display/runtime";
|
||||
import type { Subtask } from "@/core/tasks";
|
||||
import { useRehypeSplitWordsIntoSpans } from "@/core/rehype";
|
||||
import { isThreadRagModeLocked, useLocalSettings } from "@/core/settings";
|
||||
import { useSkills } from "@/core/skills/hooks";
|
||||
import {
|
||||
buildSkillDisplayMap,
|
||||
collectRuntimeDisplaySources,
|
||||
} from "@/core/skill-display/runtime";
|
||||
import type { Subtask } from "@/core/tasks";
|
||||
import { useUpdateSubtask } from "@/core/tasks/context";
|
||||
import type { AgentThreadState } from "@/core/threads";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -55,6 +55,7 @@ export function MessageList({
|
||||
thread,
|
||||
paddingBottom = MESSAGE_LIST_DEFAULT_PADDING_BOTTOM,
|
||||
tokenUsageEnabled = false,
|
||||
extraRehypePlugins,
|
||||
onClarificationSubmit,
|
||||
onSetupAgentApprove,
|
||||
onSetupAgentReject,
|
||||
@@ -64,22 +65,26 @@ export function MessageList({
|
||||
thread: BaseStream<AgentThreadState>;
|
||||
paddingBottom?: number;
|
||||
tokenUsageEnabled?: boolean;
|
||||
extraRehypePlugins?: ReturnType<typeof useRehypeSplitWordsIntoSpans>;
|
||||
onClarificationSubmit?: (answer: string) => void | Promise<void>;
|
||||
onSetupAgentApprove?: (payload: SetupAgentApprovalPayload) => void | Promise<void>;
|
||||
onSetupAgentReject?: (payload: SetupAgentApprovalPayload) => void | Promise<void>;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const rehypePlugins = useRehypeSplitWordsIntoSpans(thread.isLoading);
|
||||
const updateSubtask = useUpdateSubtask();
|
||||
const messages = thread.messages;
|
||||
const [settings] = useLocalSettings();
|
||||
const ragModeEnabled =
|
||||
settings.display.ragModeEnabled || isThreadRagModeLocked(threadId);
|
||||
const { skills } = useSkills();
|
||||
const skillDisplayMap = useMemo(
|
||||
() => (ragModeEnabled ? buildSkillDisplayMap(skills) : new Map()),
|
||||
[ragModeEnabled, skills],
|
||||
);
|
||||
const baseRehypePlugins = useRehypeSplitWordsIntoSpans(thread.isLoading);
|
||||
const rehypePlugins = useMemo(
|
||||
() => (extraRehypePlugins?.length ? [...baseRehypePlugins, ...extraRehypePlugins] : baseRehypePlugins),
|
||||
[baseRehypePlugins, extraRehypePlugins],
|
||||
);
|
||||
const updateSubtask = useUpdateSubtask();
|
||||
const messages = thread.messages;
|
||||
const [settings] = useLocalSettings();
|
||||
const ragModeEnabled =
|
||||
settings.display.ragModeEnabled || isThreadRagModeLocked(threadId);
|
||||
const { skills } = useSkills();
|
||||
const skillDisplayMap = useMemo(
|
||||
() => (ragModeEnabled ? buildSkillDisplayMap(skills) : new Map()),
|
||||
[ragModeEnabled, skills],
|
||||
);
|
||||
|
||||
// Track which clarification messages are still awaiting an answer.
|
||||
// Pending = clarification tool messages that appear after the last human message
|
||||
@@ -197,29 +202,30 @@ export function MessageList({
|
||||
<ConversationContent className="mx-auto w-full max-w-(--container-width-lg) gap-8 pt-12">
|
||||
<MessageMetricsProvider messages={messages} isLoading={thread.isLoading}>
|
||||
{groupMessages(messages, (group) => {
|
||||
if (group.type === "human" || group.type === "assistant") {
|
||||
return group.messages.map((msg) => {
|
||||
const displaySources =
|
||||
ragModeEnabled &&
|
||||
group.type === "assistant" &&
|
||||
msg.type === "ai"
|
||||
? collectRuntimeDisplaySources(messages, skillDisplayMap, {
|
||||
afterMessageId: findPreviousHumanMessageId(messages, msg.id),
|
||||
beforeMessageId: msg.id,
|
||||
includeBeforeMessage: true,
|
||||
})
|
||||
: [];
|
||||
return (
|
||||
<MessageListItem
|
||||
key={`${group.id}/${msg.id}`}
|
||||
message={msg}
|
||||
isLoading={thread.isLoading}
|
||||
threadId={threadId}
|
||||
tokenUsageEnabled={tokenUsageEnabled}
|
||||
displaySources={displaySources}
|
||||
/>
|
||||
);
|
||||
});
|
||||
if (group.type === "human" || group.type === "assistant") {
|
||||
return group.messages.map((msg) => {
|
||||
const displaySources =
|
||||
ragModeEnabled &&
|
||||
group.type === "assistant" &&
|
||||
msg.type === "ai"
|
||||
? collectRuntimeDisplaySources(messages, skillDisplayMap, {
|
||||
afterMessageId: findPreviousHumanMessageId(messages, msg.id),
|
||||
beforeMessageId: msg.id,
|
||||
includeBeforeMessage: true,
|
||||
})
|
||||
: [];
|
||||
return (
|
||||
<MessageListItem
|
||||
key={`${group.id}/${msg.id}`}
|
||||
message={msg}
|
||||
isLoading={thread.isLoading}
|
||||
threadId={threadId}
|
||||
tokenUsageEnabled={tokenUsageEnabled}
|
||||
displaySources={displaySources}
|
||||
extraRehypePlugins={extraRehypePlugins}
|
||||
/>
|
||||
);
|
||||
});
|
||||
} else if (group.type === "assistant:clarification") {
|
||||
const message = group.messages[0];
|
||||
if (message && hasContent(message)) {
|
||||
@@ -454,33 +460,33 @@ type ClarificationSelection = {
|
||||
custom: string;
|
||||
};
|
||||
|
||||
export type SetupAgentApprovalPayload = {
|
||||
name?: string;
|
||||
description?: string;
|
||||
soul?: string;
|
||||
skills?: string[];
|
||||
};
|
||||
|
||||
function findPreviousHumanMessageId(
|
||||
messages: Message[],
|
||||
beforeMessageId?: string,
|
||||
): string | undefined {
|
||||
const beforeIndex =
|
||||
beforeMessageId === undefined
|
||||
? messages.length
|
||||
: messages.findIndex((message) => message.id === beforeMessageId);
|
||||
const end = beforeIndex < 0 ? messages.length : beforeIndex;
|
||||
for (let index = end - 1; index >= 0; index--) {
|
||||
const message = messages[index];
|
||||
if (message?.type === "human") {
|
||||
return message.id;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getSetupAgentApprovalPayload(
|
||||
message: unknown,
|
||||
export type SetupAgentApprovalPayload = {
|
||||
name?: string;
|
||||
description?: string;
|
||||
soul?: string;
|
||||
skills?: string[];
|
||||
};
|
||||
|
||||
function findPreviousHumanMessageId(
|
||||
messages: Message[],
|
||||
beforeMessageId?: string,
|
||||
): string | undefined {
|
||||
const beforeIndex =
|
||||
beforeMessageId === undefined
|
||||
? messages.length
|
||||
: messages.findIndex((message) => message.id === beforeMessageId);
|
||||
const end = beforeIndex < 0 ? messages.length : beforeIndex;
|
||||
for (let index = end - 1; index >= 0; index--) {
|
||||
const message = messages[index];
|
||||
if (message?.type === "human") {
|
||||
return message.id;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getSetupAgentApprovalPayload(
|
||||
message: unknown,
|
||||
): SetupAgentApprovalPayload | null {
|
||||
if (typeof message !== "object" || message === null) {
|
||||
return null;
|
||||
|
||||
@@ -17,6 +17,7 @@ import { useI18n } from "@/core/i18n/hooks";
|
||||
import { BotIcon, BrainIcon, ListChecks, MessageSquarePlus, MessagesSquare, PenLine, SparklesIcon, Users } from "lucide-react";
|
||||
|
||||
import { SidebarCollapseButton } from "./workspace-header";
|
||||
import { WorkspaceNavStudio } from "./workspace-nav-studio";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
// 普通导航项:默认中性色,激活态与「新对话」一致——主色浅底 + 主色文字/图标
|
||||
@@ -54,6 +55,7 @@ export function WorkspaceNavChatList({
|
||||
});
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<SidebarGroup className={cn("px-2", showCollapseTrigger ? "pt-3" : "pt-2")}>
|
||||
<SidebarMenu className="gap-1">
|
||||
<SidebarMenuItem>
|
||||
@@ -163,5 +165,7 @@ export function WorkspaceNavChatList({
|
||||
)}
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
{!showCollapseTrigger && <WorkspaceNavStudio />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { BookOpen, ChevronDown, Loader2, Plus } from "lucide-react";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import {
|
||||
SidebarGroup,
|
||||
SidebarGroupLabel,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from "@/components/ui/sidebar";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useStudioNotebooks } from "@/core/studio/hooks";
|
||||
import { createSearchSpace } from "@/core/studio/api";
|
||||
import { getStoredAuth } from "@/core/auth";
|
||||
|
||||
const NAV_ITEM_CLASS =
|
||||
"text-sidebar-foreground/80 hover:text-primary rounded-xl h-9 px-3 transition-colors hover:bg-primary/10 " +
|
||||
"data-[active=true]:bg-primary/15 data-[active=true]:text-primary data-[active=true]:shadow-none " +
|
||||
"dark:hover:bg-primary/15 dark:data-[active=true]:bg-primary/25 dark:data-[active=true]:text-primary-foreground";
|
||||
|
||||
const SIDEBAR_PREVIEW_COUNT = 5;
|
||||
|
||||
export function WorkspaceNavStudio() {
|
||||
const { notebooks, isLoading, isError } = useStudioNotebooks();
|
||||
const { pathname } = useLocation();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [open, setOpen] = useState(true);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [name, setName] = useState("");
|
||||
const [creating, setCreating] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const handleCreate = async () => {
|
||||
const trimmed = name.trim();
|
||||
if (!trimmed) return;
|
||||
setCreating(true);
|
||||
setError("");
|
||||
try {
|
||||
const email = getStoredAuth()?.email ?? "";
|
||||
const token = queryClient.getQueryData<string>(["studio", "token", email]);
|
||||
if (!token) throw new Error("未登录或 token 已过期,请刷新页面");
|
||||
await createSearchSpace(trimmed, token);
|
||||
setDialogOpen(false);
|
||||
setName("");
|
||||
await queryClient.invalidateQueries({ queryKey: ["studio", "notebooks"] });
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : "创建失败");
|
||||
} finally {
|
||||
setCreating(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (isError || (!isLoading && notebooks.length === 0)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const preview = notebooks.slice(0, SIDEBAR_PREVIEW_COUNT);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Collapsible open={open} onOpenChange={setOpen}>
|
||||
<SidebarGroup className="px-2 pt-2">
|
||||
<CollapsibleTrigger asChild>
|
||||
<SidebarGroupLabel className="flex cursor-pointer select-none items-center justify-between px-1 text-xs font-medium text-sidebar-foreground/50 hover:text-sidebar-foreground/80 transition-colors">
|
||||
<span>工作室</span>
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
"size-3.5 transition-transform duration-200",
|
||||
open ? "rotate-0" : "-rotate-90",
|
||||
)}
|
||||
/>
|
||||
</SidebarGroupLabel>
|
||||
</CollapsibleTrigger>
|
||||
|
||||
<CollapsibleContent>
|
||||
<SidebarMenu className="mt-0.5 gap-0.5">
|
||||
{/* 新建笔记本 */}
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
className={cn(NAV_ITEM_CLASS)}
|
||||
onClick={() => { setDialogOpen(true); setError(""); setName(""); }}
|
||||
>
|
||||
<Plus className="shrink-0" />
|
||||
<span>新建笔记本</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
|
||||
{/* 笔记本列表(最多展示 SIDEBAR_PREVIEW_COUNT 条) */}
|
||||
{isLoading
|
||||
? Array.from({ length: 3 }).map((_, i) => (
|
||||
<SidebarMenuItem key={i}>
|
||||
<div className="mx-3 my-1 h-5 animate-pulse rounded-md bg-sidebar-accent/40" />
|
||||
</SidebarMenuItem>
|
||||
))
|
||||
: preview.map((nb) => (
|
||||
<SidebarMenuItem key={nb.id}>
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
isActive={pathname === `/page/workspace/studio/notebooks/${nb.id}`}
|
||||
className={cn(NAV_ITEM_CLASS)}
|
||||
>
|
||||
<Link
|
||||
to={`/page/workspace/studio/notebooks/${nb.id}`}
|
||||
title={nb.description || nb.name}
|
||||
>
|
||||
<BookOpen className="shrink-0 size-4" />
|
||||
<span className="truncate">{nb.name}</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
|
||||
{/* 所有笔记本 */}
|
||||
{!isLoading && notebooks.length > 0 && (
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
isActive={pathname === "/page/workspace/studio/notebooks"}
|
||||
className={cn(NAV_ITEM_CLASS)}
|
||||
>
|
||||
<Link to="/page/workspace/studio/notebooks">
|
||||
<span className="mr-0.5 text-base leading-none">···</span>
|
||||
<span>所有笔记本</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
)}
|
||||
</SidebarMenu>
|
||||
</CollapsibleContent>
|
||||
</SidebarGroup>
|
||||
</Collapsible>
|
||||
|
||||
<Dialog open={dialogOpen} onOpenChange={(v) => !creating && setDialogOpen(v)}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>新建笔记本</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-4 pt-2">
|
||||
<input
|
||||
autoFocus
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") void handleCreate(); }}
|
||||
placeholder="笔记本名称"
|
||||
disabled={creating}
|
||||
className="w-full rounded-lg border border-border bg-background px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-primary disabled:opacity-50"
|
||||
/>
|
||||
{error && <p className="text-xs text-destructive">{error}</p>}
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="outline" size="sm" disabled={creating} onClick={() => setDialogOpen(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button size="sm" disabled={!name.trim() || creating} onClick={() => void handleCreate()}>
|
||||
{creating ? <Loader2 className="size-4 animate-spin" /> : "创建"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user