This commit is contained in:
fengxiangdebaozi committed 2026-05-29 20:17:07 +08:00
commit be37779472
16 files changed
+366 -147

No files matched your search

+3 -3
View File
@@ -7,7 +7,7 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
"bg-card text-card-foreground flex flex-col gap-6 rounded-[4px] border py-6 shadow-sm dark:border-[#0049A5] dark:bg-[#02244C]",
className,
)}
{...props}
@@ -42,7 +42,7 @@ function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
className={cn("text-muted-foreground text-sm dark:text-[#F2F3F5]", className)}
{...props}
/>
);
@@ -65,7 +65,7 @@ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
className={cn("px-6 ", className)}
{...props}
/>
);
@@ -14,7 +14,7 @@ function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
data-slot="input-group"
role="group"
className={cn(
"group/input-group border-input/50 dark:bg-background/80 relative flex w-full items-center rounded-md border bg-white/80 shadow-xs transition-[color,box-shadow] outline-none",
"group/input-group relative flex w-full items-center rounded-md border border-input/50 bg-white/80 shadow-xs transition-[color,box-shadow] outline-none dark:border-[#1B477E] dark:bg-[#022248]",
"h-9 min-w-0 has-[>textarea]:h-auto",
// Variants based on alignment.
+6 -3
View File
@@ -8,9 +8,12 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
type={type}
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
"h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base outline-none transition-[color,box-shadow] md:text-sm",
"border-input placeholder:text-muted-foreground",
"dark:border-[#1B477E] dark:bg-[#022248] dark:text-[#AACEFF] dark:placeholder:text-[#566B7F] rounded-[4px] text-[16px]",
"file:text-foreground selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] dark:focus-visible:border-[#3b82f6] dark:focus-visible:ring-[#3b82f6]/30",
"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
className,
)}
{...props}
+60 -54
View File
@@ -30,7 +30,8 @@ const tabsListVariants = cva(
{
variants: {
variant: {
default: "bg-muted",
default:
"bg-muted dark:group-data-[orientation=horizontal]/tabs:h-8! dark:items-stretch dark:gap-0 dark:overflow-hidden dark:rounded-[2px] dark:border dark:border-[#55A6FE] dark:bg-[#022248] dark:p-0",
line: "gap-1 bg-transparent",
},
},
@@ -40,60 +41,65 @@ const tabsListVariants = cva(
},
);
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List> &
VariantProps<typeof tabsListVariants>
>(({ className, variant = "default", ...props }, ref) => {
return (
<TabsPrimitive.List
ref={ref}
data-slot="tabs-list"
data-variant={variant}
className={cn(tabsListVariants({ variant }), className)}
{...props}
/>
);
});
TabsList.displayName = TabsPrimitive.List.displayName;
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List> &
VariantProps<typeof tabsListVariants>
>(({ className, variant = "default", ...props }, ref) => {
return (
<TabsPrimitive.List
ref={ref}
data-slot="tabs-list"
data-variant={variant}
className={cn(tabsListVariants({ variant }), className)}
{...props}
/>
);
});
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => {
return (
<TabsPrimitive.Trigger
ref={ref}
data-slot="tabs-trigger"
className={cn(
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 hover:text-foreground dark:text-muted-foreground dark:hover:text-foreground relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent",
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 data-[state=active]:text-foreground",
"after:bg-foreground after:absolute after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100",
className,
)}
{...props}
/>
);
});
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
TabsList.displayName = TabsPrimitive.List.displayName;
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => {
return (
<TabsPrimitive.Content
ref={ref}
data-slot="tabs-content"
className={cn("flex-1 outline-none", className)}
{...props}
/>
);
});
TabsContent.displayName = TabsPrimitive.Content.displayName;
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => {
return (
<TabsPrimitive.Trigger
ref={ref}
data-slot="tabs-trigger"
className={cn(
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 hover:text-foreground relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] outline-none group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"dark:text-muted-foreground dark:hover:text-foreground",
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent",
"data-[state=active]:bg-background data-[state=active]:text-foreground",
"dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-input dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-input/30 dark:group-data-[variant=line]/tabs-list:data-[state=active]:text-foreground",
"dark:group-data-[variant=default]/tabs-list:relative dark:group-data-[variant=default]/tabs-list:!h-full dark:group-data-[variant=default]/tabs-list:self-stretch dark:group-data-[variant=default]/tabs-list:flex-none dark:group-data-[variant=default]/tabs-list:rounded-none dark:group-data-[variant=default]/tabs-list:border-0 dark:group-data-[variant=default]/tabs-list:bg-transparent dark:group-data-[variant=default]/tabs-list:px-4 dark:group-data-[variant=default]/tabs-list:!py-0 dark:group-data-[variant=default]/tabs-list:!leading-none dark:group-data-[variant=default]/tabs-list:shadow-none dark:group-data-[variant=default]/tabs-list:focus-visible:ring-0 dark:group-data-[variant=default]/tabs-list:[&_svg]:text-[#CCD2DA]",
"dark:group-data-[variant=default]/tabs-list:not(:first-child):border-l dark:group-data-[variant=default]/tabs-list:not(:first-child):border-[#1d3961] dark:group-data-[variant=default]/tabs-list:data-[state=active]:border-l-0",
"dark:group-data-[variant=default]/tabs-list:data-[state=active]:rounded-none dark:group-data-[variant=default]/tabs-list:data-[state=active]:border-0 dark:group-data-[variant=default]/tabs-list:data-[state=active]:bg-[#0062D9] dark:group-data-[variant=default]/tabs-list:data-[state=active]:text-[#FFFFFF] dark:group-data-[variant=default]/tabs-list:data-[state=active]:shadow-none dark:group-data-[variant=default]/tabs-list:data-[state=active]:[&_svg]:text-[#FFFFFF] dark:group-data-[variant=default]/tabs-list:hover:text-[#CCD2DA]",
"after:bg-foreground after:absolute after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100",
className,
)}
{...props}
/>
);
});
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => {
return (
<TabsPrimitive.Content
ref={ref}
data-slot="tabs-content"
className={cn("flex-1 outline-none", className)}
{...props}
/>
);
});
TabsContent.displayName = TabsPrimitive.Content.displayName;
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants };
+3 -1
View File
@@ -7,7 +7,9 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
<textarea
data-slot="textarea"
className={cn(
"border-input placeholder:text-muted-foreground/60 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"border-input placeholder:text-muted-foreground/60 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"dark:border-[#1B477E] dark:bg-[#022248] dark:text-[#AACEFF] ",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className,
)}
{...props}
@@ -267,15 +267,18 @@ export function AgentCard({ agent }: AgentCardProps) {
return (
<>
<Card className="group flex flex-col transition-shadow hover:shadow-md">
<CardHeader className="pb-3">
<div className="flex items-start justify-between gap-2">
<div className="flex min-w-0 flex-1 items-center gap-2">
<Card className="group flex min-w-0 flex-col transition-shadow hover:shadow-md">
<CardHeader className="min-w-0 pb-3">
<div className="flex min-w-0 items-start justify-between gap-2">
<div className="flex min-w-0 flex-1 items-center gap-2 overflow-hidden">
<div className="bg-primary/10 text-primary flex h-9 w-9 shrink-0 items-center justify-center rounded-lg">
<BotIcon className="h-5 w-5" />
</div>
<div className="min-w-0 flex-1">
<CardTitle className="truncate text-base" title={desensitize(agent.name)}>
<div className="min-w-0 flex-1 overflow-hidden">
<CardTitle
className="block w-full min-w-0 overflow-hidden text-base text-ellipsis whitespace-nowrap"
title={desensitize(agent.name)}
>
{desensitize(agent.name)}
</CardTitle>
{agent.model && (
@@ -146,15 +146,21 @@ export function AgentGallery() {
const renderEmpty = (description: string, showNewButton = false) => (
<div className="flex h-64 flex-col items-center justify-center gap-3 text-center">
<div className="bg-muted flex h-14 w-14 items-center justify-center rounded-full">
<BotIcon className="text-muted-foreground h-7 w-7" />
<div className="bg-muted flex h-14 w-14 items-center justify-center rounded-full dark:bg-[#022248]">
<BotIcon className="text-muted-foreground h-7 w-7 dark:text-[#AACEFF]" />
</div>
<div>
<p className="font-medium">{t.agents.emptyTitle}</p>
<p className="text-muted-foreground mt-1 text-sm">{description}</p>
<p className="font-medium dark:text-[#AACEFF]">{t.agents.emptyTitle}</p>
<p className="text-muted-foreground mt-1 text-sm dark:text-[#55A6FE]">
{description}
</p>
</div>
{showNewButton && (
<Button variant="outline" className="mt-2" onClick={handleNewAgent}>
<Button
variant="outline"
className="mt-2 dark:border-[#2580FF] dark:bg-[#2580FF] dark:text-white dark:hover:border-[#2580FF] dark:hover:bg-[#2580FF]/90 dark:hover:text-white"
onClick={handleNewAgent}
>
<PlusIcon className="mr-1.5 h-4 w-4" />
{t.agents.newAgent}
</Button>
@@ -68,7 +68,7 @@ export function TagSearchBar({
return (
<div className={cn("flex flex-col gap-2.5", className)}>
<div className="relative">
<SearchIcon className="text-muted-foreground absolute left-2.5 top-1/2 size-4 -translate-y-1/2" />
<SearchIcon className="text-muted-foreground dark:text-[#60a5fa]/90 absolute left-2.5 top-1/2 size-4 -translate-y-1/2" />
<Input
value={input}
onChange={(e) => setInput(e.target.value)}
+105 -35
View File
@@ -19,6 +19,13 @@ import {
} from "@/components/ai-elements/prompt-input";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { ArtifactsProvider } from "@/components/workspace/artifacts";
import {
@@ -115,10 +122,34 @@ export default function NewAgentPage() {
return;
}
const first = models[0]!.name;
setModelInput((prev) => prev || first);
setChatModelInput((prev) => prev || first);
setModelInput((prev) =>
prev && models.some((m) => m.name === prev) ? prev : first,
);
setChatModelInput((prev) =>
prev && models.some((m) => m.name === prev) ? prev : first,
);
}, [models]);
const resolvedModelInput = useMemo(() => {
if (models.length === 0) {
return "";
}
if (modelInput && models.some((m) => m.name === modelInput)) {
return modelInput;
}
return models[0]!.name;
}, [modelInput, models]);
const resolvedChatModelInput = useMemo(() => {
if (models.length === 0) {
return "";
}
if (chatModelInput && models.some((m) => m.name === chatModelInput)) {
return chatModelInput;
}
return models[0]!.name;
}, [chatModelInput, models]);
const [thread, sendMessage] = useThreadStream({
threadId,
assistantId: agentId || undefined,
@@ -347,23 +378,42 @@ export default function NewAgentPage() {
<span className="text-muted-foreground text-xs">
当前对话模型
</span>
<select
value={chatModelInput}
onChange={(event) =>
setChatModelInput(event.currentTarget.value)
}
disabled={isModelsLoading || thread.isLoading}
className="border-input bg-background h-7 min-w-[10rem] rounded-md border px-2 text-xs shadow-xs outline-none"
>
<option value="" disabled>
选择模型
</option>
{models.map((model) => (
<option key={model.name} value={model.name}>
{model.display_name || model.name}
</option>
))}
</select>
{isModelsLoading || models.length === 0 ? (
<div className="border-input bg-background text-muted-foreground flex h-7 min-w-[10rem] items-center rounded-md border px-2 text-xs shadow-xs dark:border-[#1B477E] dark:bg-[#022248]">
{isModelsLoading ? "加载中…" : "暂无模型"}
</div>
) : (
<Select
value={resolvedChatModelInput}
onValueChange={setChatModelInput}
disabled={thread.isLoading}
>
<SelectTrigger
className={cn(
"border-input bg-background h-7 min-w-[10rem] px-2 text-xs shadow-xs outline-none",
"dark:border-[#1B477E] dark:bg-[#022248] dark:text-[#AACEFF] dark:hover:bg-[#022248]",
"data-[placeholder]:text-muted-foreground",
"[&_svg]:dark:text-[#AACEE2]",
)}
>
<SelectValue placeholder="选择模型" />
</SelectTrigger>
<SelectContent
position="popper"
className="w-[var(--radix-select-trigger-width)] dark:border-[#1B477E] dark:bg-[#022248]"
>
{models.map((model) => (
<SelectItem
key={model.name}
value={model.name}
className="dark:text-[#AACEFF] dark:data-[highlighted]:bg-[#0062D9] dark:data-[highlighted]:text-white"
>
{model.display_name || model.name}
</SelectItem>
))}
</SelectContent>
</Select>
)}
</div>
<PromptInput
onSubmit={({ text }) => void handleChatSubmit(text)}
@@ -415,21 +465,41 @@ export default function NewAgentPage() {
<div className="space-y-2">
<label className="text-sm font-medium">模型</label>
<select
value={modelInput}
onChange={(event) => setModelInput(event.currentTarget.value)}
disabled={isModelsLoading}
className="border-input bg-background h-9 w-full rounded-md border px-3 text-sm shadow-xs outline-none"
>
<option value="" disabled>
选择模型
</option>
{models.map((model) => (
<option key={model.name} value={model.name}>
{model.display_name || model.name}
</option>
))}
</select>
{isModelsLoading || models.length === 0 ? (
<div className="border-input bg-background text-muted-foreground flex h-9 w-full items-center rounded-md border px-3 text-sm shadow-xs dark:border-[#1B477E] dark:bg-[#022248]">
{isModelsLoading ? "加载模型…" : "暂无可用模型"}
</div>
) : (
<Select
value={resolvedModelInput}
onValueChange={setModelInput}
>
<SelectTrigger
className={cn(
"border-input bg-background h-9 w-full shadow-xs outline-none",
"dark:border-[#1B477E] dark:bg-[#022248] dark:text-[#AACEFF] dark:hover:bg-[#022248]",
"data-[placeholder]:text-muted-foreground",
"[&_svg]:dark:text-[#AACEE2]",
)}
>
<SelectValue placeholder="选择模型" />
</SelectTrigger>
<SelectContent
position="popper"
className="w-[var(--radix-select-trigger-width)] dark:border-[#1B477E] dark:bg-[#022248]"
>
{models.map((model) => (
<SelectItem
key={model.name}
value={model.name}
className="dark:text-[#AACEFF] dark:data-[highlighted]:bg-[#0062D9] dark:data-[highlighted]:text-white"
>
{model.display_name || model.name}
</SelectItem>
))}
</SelectContent>
</Select>
)}
<p className="text-muted-foreground text-xs">
该智能体被使用时调用的模型。
</p>
@@ -463,7 +533,7 @@ export default function NewAgentPage() {
/>
</div>
)}
<div className="max-h-[240px] overflow-y-auto rounded-md border p-2">
<div className="max-h-[240px] overflow-y-auto rounded-md border p-2 dark:border-[#1B477E] dark:bg-[#022248]">
{isSkillsLoading ? (
<p className="text-muted-foreground text-sm">技能加载中...</p>
) : enabledSkills.length === 0 ? (
@@ -435,14 +435,19 @@ async def list_agents(
agents = await store.list_all()
else:
agents = await store.list_visible(user_id)
# List endpoint stays lean: skip per-agent SOUL.md read, config.yaml
# parse, and dir self-heal — none of those feed UI fields the gallery /
# recommend dialog / selector actually render. Detail endpoints (single
# agent + create/update responses) still load them.
responses = [
_record_to_response(a, include_soul=False, include_extras=False)
for a in agents
]
# Load lightweight extras (skills / model / tool_groups from config.yaml)
# so the gallery cards can render skill badges. SOUL.md is still skipped
# (only the edit dialog reads it). The per-agent config.yaml reads run in
# worker threads concurrently, so the event loop stays responsive even
# with many agents.
responses = list(
await asyncio.gather(
*(
_record_to_response_async(a, include_soul=False, include_extras=True)
for a in agents
)
)
)
assignments = await tag_store.list_assignments("agent", [r.id for r in responses])
favorite_ids = await store.list_favorite_ids(user_id)
@@ -105,7 +105,8 @@ _BUILTIN_GRAPH_IDS: frozenset[str] = frozenset({"lead_agent", "canvas_agent", "a
_RAG_CITATION_PROMPT = """当前对话已开启参考文献模式。
如果本轮调用了搜索、知识库、检索类技能或工具,请基于本轮所有检索结果整体回答。
你必须把同一轮内多次检索得到的资料视为同一批参考文献,按出现顺序从 [1] 开始连续编号,不要让每个检索词各自从 [1] 重新编号。
如果你已经知道需要多个检索词,请优先在同一步里并行调用检索工具,等这些检索都返回后再回答,不要查一个检索词就先写一版答案。
你必须把同一轮内多次检索得到的资料视为同一个 referenceBatch,按系统稍后提供的合并批次从 [1] 开始连续编号,不要让每个检索词各自从 [1] 重新编号。
回答正文中的关键结论必须标注对应编号,例如 [1]、[2],多个来源可写为 [1][3][5]。
不要引用不存在的编号,不要使用技能内部自带的编号,不要把参考文献完整列表堆在正文末尾。
如果没有任何可用检索结果,请直接说明未检索到可靠参考资料。"""
+3 -3
View File
@@ -147,14 +147,14 @@ agents_api:
enabled: true
database:
backend: mysql
mysql_url: $MYSQL_DATABASE_URL
backend: sqlite
sqlite_dir: .deer-flow/data
pool_size: 10
echo_sql: false
checkpointer:
type: sqlite
connection_string: D:\cursor-work\offline-backend-20260512\backend\.deer-flow\data\deerflow.db
connection_string: D:\05271502\deerflow-server\offline-backend-20260512\backend\.deer-flow\data\deerflow.db
# 记忆子系统:跨会话持久化用户画像与 agent 私有记忆。
# 三层覆盖优先级(高->低):
@@ -703,12 +703,14 @@ def _get_cached_skills_prompt_section(
display_pattern = (
"""
**Skill Result Display Pattern:**
- If a skill has `<display mode="citation">`, use the skill's returned result order as reference numbers. Cite facts from the first result as `[[1]]`, the second as `[[2]]`, and so on. Use only numbers that correspond to returned results.
- If a skill has `<display mode="list">`, summarize the returned items as a concise list and keep item order stable.
- If a skill has `<display mode="table">`, summarize the result and output `<skill-display tool="SKILL_OR_TOOL_NAME" mode="table" />` where the frontend should render the real table. Do not hand-write the table rows yourself.
- If a skill has `<display mode="card">`, summarize the result and output `<skill-display tool="SKILL_OR_TOOL_NAME" mode="card" />` where the frontend should render the cards.
- If a skill has `<display mode="custom">`, follow the custom display instruction in the `<display>` tag when presenting the skill result.
**Skill Result Display Pattern:**
- If RAG/reference display mode is enabled, do not number each skill result independently. Treat all retrieval results from the same user turn as one combined `referenceBatch` and cite with `[1]`, `[2]`, etc. from the system-provided reference prompt.
- In RAG/reference display mode, do not output `<skill-display .../>`; the frontend renders cards, tables, lists, and citation details in the right reference panel.
- If RAG/reference display mode is not enabled and a skill has `<display mode="citation">`, use the skill's returned result order as reference numbers. Cite facts from the first result as `[1]`, the second as `[2]`, and so on. Use only numbers that correspond to returned results.
- If a skill has `<display mode="list">`, summarize the returned items as a concise list and keep item order stable.
- If a skill has `<display mode="table">`, summarize the result and output `<skill-display tool="SKILL_OR_TOOL_NAME" mode="table" />` where the frontend should render the real table. Do not hand-write the table rows yourself.
- If a skill has `<display mode="card">`, summarize the result and output `<skill-display tool="SKILL_OR_TOOL_NAME" mode="card" />` where the frontend should render the cards.
- If a skill has `<display mode="custom">`, follow the custom display instruction in the `<display>` tag when presenting the skill result.
"""
if display_signature
else ""
@@ -993,16 +995,16 @@ Writing mode is enabled for this run.
network_policy_section = _build_network_policy_section()
rag_mode_section = (
"""
<rag_citation_mode>
RAG/reference display mode is enabled.
When a search, knowledge, skill, or MCP tool returns a result list:
- If the skill has a display adapter, follow that adapter first.
- For `citation`, cite returned items with `[[1]]`, `[[2]]`, etc. Only cite real returned items.
- For `table`, do not hand-write the table. Summarize and place `<skill-display tool="SKILL_OR_TOOL_NAME" mode="table" />` where the table should appear. The frontend will render the real tool result as a stable table.
- For `list` or `card`, summarize and place `<skill-display tool="SKILL_OR_TOOL_NAME" mode="list" />` or `<skill-display tool="SKILL_OR_TOOL_NAME" mode="card" />` where the structured result should appear.
- If no display adapter exists, default to citation mode with numbered citations.
</rag_citation_mode>
"""
<rag_citation_mode>
RAG/reference display mode is enabled.
When a search, knowledge, skill, or MCP tool returns a result list:
- If multiple search terms are needed, prefer calling the retrieval tools for all known terms in the same step, then answer after the tool results are returned.
- Treat every retrieval result from the same user turn as one combined `referenceBatch`; do not restart numbering for each search term, tool call, skill, or display adapter.
- Use only the system-provided merged reference numbers in the final answer, formatted as `[1]`, `[2]`, `[1][3][5]`.
- Do not output `<skill-display .../>` in RAG/reference mode. The frontend renders configured cards, tables, lists, and citation details in the right reference panel.
- If no display adapter exists, the frontend defaults the right reference panel to numbered citations.
</rag_citation_mode>
"""
if rag_mode_enabled
else ""
)
@@ -80,13 +80,15 @@ class RagCitationMiddleware(AgentMiddleware[AgentState]):
return None
summary = build_reference_debug_summary(messages, [batch] if batch else None)
_log_rag_debug(
"middleware reference-prompt enabled=true prompt=true messages=%s tool_messages=%s "
"raw_items=%s normalized_items=%s kept_items=%s latest_batch_sources=%s",
"middleware reference-prompt enabled=true prompt=true single_batch=true messages=%s "
"tool_messages=%s raw_items=%s normalized_items=%s kept_items=%s latest_batch_id=%s "
"latest_batch_sources=%s",
summary["messages"],
summary["tool_messages"],
summary["raw_items"],
summary["normalized_items"],
summary["kept_items"],
(batch or {}).get("id"),
len((batch or {}).get("sources") or []),
)
_log_rag_debug(
@@ -37,24 +37,45 @@ def build_reference_batches(messages: list[Any]) -> list[dict[str, Any]]:
return batches
def build_latest_reference_prompt(messages: list[Any]) -> tuple[str, dict[str, Any] | None]:
def build_latest_reference_batch(messages: list[Any]) -> dict[str, Any] | None:
"""Return the single combined citation batch for the latest human turn."""
human_index = _find_latest_human_index(messages)
if human_index is None:
return "", None
return None
sources = _collect_sources(messages[human_index + 1 :])
if not sources:
return "", None
return None
assistant = _find_reference_batch_anchor(messages, human_index + 1, len(messages))
message_id = _message_id(assistant) or _message_id(messages[human_index]) or str(human_index)
batch = {
return {
"id": message_id,
"assistant_message_id": message_id,
"source_count": len(sources),
"sources": sources,
}
def build_latest_reference_prompt(messages: list[Any]) -> tuple[str, dict[str, Any] | None]:
batch = build_latest_reference_batch(messages)
if batch is None:
return "", None
return build_reference_prompt_for_batch(batch), batch
def build_reference_prompt_for_batch(batch: dict[str, Any]) -> str:
"""Build the authoritative model prompt from one already-combined batch."""
sources = [
source
for source in batch.get("sources", [])
if isinstance(source, dict)
]
source_count = len(sources)
lines = [
"本轮检索结果已由系统统一合并并重新编号。最终回答必须使用下面这套编号。",
"如果你仍需要补充检索,请先继续调用工具;最终回答只能引用最新合并后的编号。",
f"本轮所有检索结果已合并为同一个 referenceBatch,共 {source_count} 条,编号从 [1] 到 [{source_count}] 唯一连续。",
"下面这一批就是当前回答唯一可用的参考文献批次;不要按检索词、工具调用次数或工具原始编号重新分批编号。",
"如果你仍需要补充检索,请只继续调用工具,不要输出正文;最终回答只能引用补充检索后系统重新合并出的最新 referenceBatch。",
"",
f"<reference_batch source_count=\"{source_count}\">",
]
for source in sources:
title = source.get("title") or f"参考文献 {source.get('index')}"
@@ -73,18 +94,20 @@ def build_latest_reference_prompt(messages: list[Any]) -> tuple[str, dict[str, A
if content:
lines.append(content)
lines.append("")
lines.append("</reference_batch>")
lines.append("")
lines.extend(
[
"引用规则:",
"1. 回答正文中的关键结论必须标注对应编号,例如 [1]、[2]。",
"2. 多个来源可写为 [1][3][5]。",
"3. 只能引用上方存在的编号,不要编造编号。",
"4. 不要使用工具原始编号,也不要重新排序编号。",
"4. 不要使用工具原始编号,也不要重新排序、重新分批或重新从 [1] 开始编号。",
"5. 不要在正文末尾堆完整参考文献列表,前端会展示右侧参考文献。",
"6. 参考文献模式下不要输出 <skill-display .../> 占位符,也不要在正文里额外渲染卡片、表格或列表;前端会在右侧参考文献区域展示。",
]
)
return "\n".join(lines).strip(), batch
return "\n".join(lines).strip()
def _find_latest_human_index(messages: list[Any]) -> int | None:
@@ -0,0 +1,96 @@
"""Tests for RAG citation batch construction."""
from __future__ import annotations
import json
from deerflow.runtime.references import (
build_latest_reference_batch,
build_latest_reference_prompt,
build_reference_batches,
)
def _tool_message(call_id: str, query: str, start: int) -> dict:
return {
"type": "tool",
"name": "web_search",
"tool_call_id": call_id,
"content": json.dumps(
{
"query": query,
"results": [
{
"title": f"{query} result {start}",
"content": f"{query} content {start}",
"url": f"https://example.test/{start}",
},
{
"title": f"{query} result {start + 1}",
"content": f"{query} content {start + 1}",
"url": f"https://example.test/{start + 1}",
},
],
},
ensure_ascii=False,
),
}
def test_latest_reference_prompt_uses_one_combined_batch_for_multi_search_turn() -> None:
messages = [
{"type": "human", "id": "human-1", "content": "search two terms"},
{
"type": "ai",
"id": "ai-search-1",
"content": "",
"tool_calls": [{"id": "call-1", "name": "web_search", "args": {"query": "Trump"}}],
},
_tool_message("call-1", "Trump", 1),
{
"type": "ai",
"id": "ai-search-2",
"content": "",
"tool_calls": [{"id": "call-2", "name": "web_search", "args": {"query": "White House"}}],
},
_tool_message("call-2", "White House", 3),
]
batch = build_latest_reference_batch(messages)
assert batch is not None
assert batch["source_count"] == 4
assert [source["index"] for source in batch["sources"]] == [1, 2, 3, 4]
assert [source["title"] for source in batch["sources"]] == [
"Trump result 1",
"Trump result 2",
"White House result 3",
"White House result 4",
]
prompt, prompt_batch = build_latest_reference_prompt(messages)
assert prompt_batch == batch
assert '<reference_batch source_count="4">' in prompt
assert "[1] Trump result 1" in prompt
assert "[4] White House result 4" in prompt
assert "重新分批" in prompt
def test_reference_batches_keep_separate_user_turns_but_merge_searches_inside_turn() -> None:
messages = [
{"type": "human", "id": "human-1", "content": "first"},
{"type": "ai", "id": "ai-1", "content": "", "tool_calls": [{"id": "c1", "name": "web_search"}]},
_tool_message("c1", "first", 1),
{"type": "ai", "id": "answer-1", "content": "answer [1]"},
{"type": "human", "id": "human-2", "content": "second"},
{"type": "ai", "id": "ai-2", "content": "", "tool_calls": [{"id": "c2", "name": "web_search"}]},
_tool_message("c2", "second-a", 3),
{"type": "ai", "id": "ai-3", "content": "", "tool_calls": [{"id": "c3", "name": "web_search"}]},
_tool_message("c3", "second-b", 5),
{"type": "ai", "id": "answer-2", "content": "answer [1][4]"},
]
batches = build_reference_batches(messages)
assert [len(batch["sources"]) for batch in batches] == [2, 4]
assert [source["index"] for source in batches[1]["sources"]] == [1, 2, 3, 4]
assert batches[0]["assistant_message_id"] == "answer-1"
assert batches[1]["assistant_message_id"] == "answer-2"