Enhance workflow routing and agent configuration management

- Introduce WorkflowLLMRouter for pre-response LLM routing, allowing agents to determine the appropriate function to call based on user input.
- Implement UserTurnRoutingProcessor to manage user turns before reaching the LLM, ensuring proper routing and handling of user messages.
- Refactor WorkflowBrain to integrate new routing logic and enhance agent stage configuration, including entry modes and resource management.
- Update service factory to support dynamic LLM resource configuration based on workflow settings.
- Add tests for new routing functionality and ensure proper handling of user messages in various scenarios.
This commit is contained in:
Xin Wang
2026-07-14 09:36:28 +08:00
parent 32aef14ddb
commit 72856bf3a7
19 changed files with 2611 additions and 552 deletions

View File

@@ -0,0 +1,168 @@
"use client";
import { Settings2 } from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import type { KnowledgeRetrievalConfig } from "@/lib/api";
export const DEFAULT_KNOWLEDGE_RETRIEVAL_CONFIG: KnowledgeRetrievalConfig = {
mode: "automatic",
topN: 5,
scoreThreshold: 0,
};
export function KnowledgeRetrievalConfigDialog({
disabled,
value,
onChange,
}: {
disabled: boolean;
value: KnowledgeRetrievalConfig;
onChange: (config: KnowledgeRetrievalConfig) => void;
}) {
const [open, setOpen] = useState(false);
const [draft, setDraft] = useState(value);
const [error, setError] = useState<string | null>(null);
function openDialog() {
setDraft(value);
setError(null);
setOpen(true);
}
function saveDraft() {
if (draft.topN === 0 || draft.topN < -1 || !Number.isInteger(draft.topN)) {
setError("Top N 必须为 -1 或大于 0 的整数");
return;
}
if (draft.scoreThreshold < 0 || draft.scoreThreshold > 1) {
setError("最低相关度必须在 0 到 1 之间");
return;
}
onChange(draft);
setOpen(false);
}
return (
<>
<button
type="button"
disabled={disabled}
onClick={openDialog}
aria-label="打开知识库高级配置"
title={
disabled
? "请先选择知识库"
: `${value.mode === "automatic" ? "自动检索" : "模型主动检索"} · Top N ${value.topN === -1 ? "不限" : value.topN} · 最低相关度 ${value.scoreThreshold}`
}
className="flex h-5 w-5 items-center justify-center rounded-full text-muted-soft transition-colors hover:bg-surface-strong hover:text-foreground disabled:cursor-not-allowed disabled:opacity-40"
>
<Settings2 size={14} />
</button>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
<div className="space-y-5 py-2">
<div className="space-y-2">
<div className="text-sm font-medium text-foreground"></div>
<Select
value={draft.mode}
onValueChange={(mode: "automatic" | "on_demand") =>
setDraft({ ...draft, mode })
}
>
<SelectTrigger className="w-full border-hairline-strong bg-background">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="automatic"></SelectItem>
<SelectItem value="on_demand"></SelectItem>
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">
{draft.mode === "automatic"
? "每轮用户提问后自动检索,响应行为更稳定。"
: "由大模型判断是否调用知识库,依赖模型的工具调用能力。"}
</p>
</div>
<label className="block">
<span className="mb-2 block text-sm font-medium text-foreground">
</span>
<Input
type="number"
step="1"
min="-1"
value={draft.topN}
onChange={(event) =>
setDraft({ ...draft, topN: Number(event.target.value) })
}
/>
<span className="mt-1.5 block text-xs text-muted-foreground">
-1
</span>
</label>
<label className="block">
<span className="mb-2 block text-sm font-medium text-foreground">
</span>
<Input
type="number"
step="0.01"
min="0"
max="1"
value={draft.scoreThreshold}
onChange={(event) =>
setDraft({
...draft,
scoreThreshold: Number(event.target.value),
})
}
/>
<span className="mt-1.5 block text-xs text-muted-foreground">
01
</span>
</label>
{error && <p className="text-sm text-destructive">{error}</p>}
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setOpen(false)}>
</Button>
<Button type="button" onClick={saveDraft}>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}

View File

@@ -0,0 +1,92 @@
"use client";
/**
* Compact section chrome shared by assistant editors and workflow node panels.
* Density matches the debug preview drawer (text-sm titles, tight padding).
*/
import { HelpCircle } from "lucide-react";
import type { ReactNode } from "react";
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { cn } from "@/lib/utils";
export function SectionCard({
icon,
title,
description,
children,
className,
}: {
icon?: ReactNode;
title?: string;
description?: string;
children: ReactNode;
className?: string;
}) {
const hasHeader = Boolean(title);
return (
<Card
size="sm"
className={cn(
"gap-3 rounded-2xl border border-hairline bg-card py-3.5 text-card-foreground shadow-sm ring-0",
className,
)}
>
{hasHeader && (
<CardHeader className="gap-0 px-4">
<div className="flex items-center gap-2.5">
{icon && (
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-surface-strong text-foreground">
{icon}
</div>
)}
<div className="flex min-w-0 items-center gap-1.5">
<CardTitle className="text-sm font-medium leading-none">
{title}
</CardTitle>
{description && <HelpHint text={description} />}
</div>
</div>
</CardHeader>
)}
<CardContent className={cn("px-4", hasHeader && "space-y-3")}>
{children}
</CardContent>
</Card>
);
}
export function HelpHint({ text }: { text: string }) {
return (
<Popover>
<PopoverTrigger asChild>
<button
type="button"
aria-label="查看说明"
onClick={(event) => event.stopPropagation()}
className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-muted-soft transition-colors hover:bg-surface-strong hover:text-foreground"
>
<HelpCircle size={13} />
</button>
</PopoverTrigger>
<PopoverContent
align="start"
className="w-72 text-sm leading-6 text-muted-foreground"
>
{text}
</PopoverContent>
</Popover>
);
}

View File

@@ -21,7 +21,6 @@ import {
Save,
Mic,
Send,
HelpCircle,
Waypoints,
AudioLines,
Terminal,
@@ -87,12 +86,6 @@ import { PageHeader } from "@/components/ui/page-header";
import { FilterPills } from "@/components/ui/filter-pills";
import { SearchInput } from "@/components/ui/search-input";
import { ListToolbar } from "@/components/ui/list-toolbar";
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { useCallback, useEffect, useRef, useState } from "react";
import { useRouter } from "next/navigation";
import {
@@ -124,6 +117,7 @@ import {
WorkflowEditor,
type WorkflowSettings,
} from "@/components/workflow/WorkflowEditor";
import { HelpHint, SectionCard } from "@/components/editor/section-card";
import {
defaultGraph,
type WorkflowGraph,
@@ -362,7 +356,7 @@ type AssistantTypeOption = {
label: string;
description: string;
icon: React.ReactNode;
/** 提示词、Dify、FastGPT 类型已落地,工作流暂时显示占位页 */
/** 提示词、工作流、Dify、FastGPT 已落地OpenCode 暂时显示即将上线 */
available: boolean;
};
@@ -379,7 +373,7 @@ const assistantTypeOptions: AssistantTypeOption[] = [
label: "使用工作流构建",
description: "用可视化编排串联多个节点,适合多步骤、带分支的复杂流程。",
icon: <Workflow size={20} />,
available: false,
available: true,
},
{
type: "Dify",
@@ -400,7 +394,7 @@ const assistantTypeOptions: AssistantTypeOption[] = [
label: "使用 OpenCode 构建",
description: "对接 OpenCode 服务,通过提示词驱动代码助手并支持实时语音对话。",
icon: <Terminal size={20} />,
available: true,
available: false,
},
];
@@ -472,12 +466,23 @@ export function AssistantPage(props: AssistantPageProps) {
);
const [workflowSettings, setWorkflowSettings] = useState<WorkflowSettings>({
globalPrompt: defaultGraph().settings.globalPrompt,
llm: defaultGraph().settings.defaultLlmResourceId,
asr: defaultGraph().settings.defaultAsrResourceId,
tts: defaultGraph().settings.defaultTtsResourceId,
toolIds: defaultGraph().settings.toolIds,
knowledgeBaseId: defaultGraph().settings.knowledgeBaseId,
knowledgeRetrievalConfig: {
mode: defaultGraph().settings.knowledgeMode,
topN: defaultGraph().settings.knowledgeTopN,
scoreThreshold: defaultGraph().settings.knowledgeScoreThreshold,
},
allowInterrupt: true,
turnConfig: defaultTurnConfig(),
});
const [workflowDynamicVariableDefinitions, setWorkflowDynamicVariableDefinitions] =
useState<Record<string, DynamicVariableDefinition>>({});
const [workflowDebugOpen, setWorkflowDebugOpen] = useState(false);
const [workflowSettingsOpen, setWorkflowSettingsOpen] = useState(false);
const [workflowEditingNodeId, setWorkflowEditingNodeId] = useState<string | null>(null);
const [workflowEditingEdgeId, setWorkflowEditingEdgeId] = useState<string | null>(null);
const [activeNodeId, setActiveNodeId] = useState<string | null>(null);
@@ -847,9 +852,25 @@ export function AssistantPage(props: AssistantPageProps) {
? (assistant.graph as WorkflowGraph)
: defaultGraph();
const wfSettings: WorkflowSettings = {
llm: assistant.modelResourceIds.LLM,
llm:
graph.settings?.defaultLlmResourceId ||
assistant.modelResourceIds.LLM,
asr: graph.settings?.defaultAsrResourceId || assistant.modelResourceIds.ASR,
tts: graph.settings?.defaultTtsResourceId || assistant.modelResourceIds.TTS,
toolIds: graph.settings?.toolIds ?? [],
knowledgeBaseId:
graph.settings?.knowledgeBaseId || assistant.knowledgeBaseId || "",
knowledgeRetrievalConfig: {
mode:
graph.settings?.knowledgeMode ||
assistant.knowledgeRetrievalConfig.mode,
topN:
graph.settings?.knowledgeTopN ??
assistant.knowledgeRetrievalConfig.topN,
scoreThreshold:
graph.settings?.knowledgeScoreThreshold ??
assistant.knowledgeRetrievalConfig.scoreThreshold,
},
globalPrompt: graph.settings?.globalPrompt ?? "",
allowInterrupt: assistant.enableInterrupt,
turnConfig: assistant.turnConfig,
@@ -916,6 +937,9 @@ export function AssistantPage(props: AssistantPageProps) {
...(workflowSettings.asr ? { ASR: workflowSettings.asr } : {}),
...(workflowSettings.tts ? { TTS: workflowSettings.tts } : {}),
},
knowledgeBaseId: workflowSettings.knowledgeBaseId || null,
knowledgeRetrievalConfig: workflowSettings.knowledgeRetrievalConfig,
toolIds: workflowSettings.toolIds,
graph: workflowGraph as unknown as Record<string, unknown>,
dynamicVariableDefinitions: workflowDynamicVariableDefinitions,
}),
@@ -1386,7 +1410,7 @@ export function AssistantPage(props: AssistantPageProps) {
<span
role="alert"
title={saveError}
className="line-clamp-2 max-w-[min(42vw,560px)] self-center text-right text-sm leading-5 text-destructive"
className="line-clamp-1 max-w-[min(42vw,560px)] self-center text-right text-sm leading-5 text-destructive"
>
{saveError}
</span>
@@ -1396,6 +1420,7 @@ export function AssistantPage(props: AssistantPageProps) {
className="gap-2 border-hairline-strong text-foreground hover:bg-surface-strong"
disabled={!editingId}
onClick={() => {
setWorkflowSettingsOpen(false);
setWorkflowEditingNodeId(null);
setWorkflowEditingEdgeId(null);
setWorkflowDebugOpen(true);
@@ -1430,6 +1455,8 @@ export function AssistantPage(props: AssistantPageProps) {
onEditingNodeIdChange={setWorkflowEditingNodeId}
editingEdgeId={workflowEditingEdgeId}
onEditingEdgeIdChange={setWorkflowEditingEdgeId}
settingsOpen={workflowSettingsOpen}
onSettingsOpenChange={setWorkflowSettingsOpen}
debugOpen={workflowDebugOpen}
onDebugOpenChange={(open) => {
setWorkflowDebugOpen(open);
@@ -1508,10 +1535,10 @@ export function AssistantPage(props: AssistantPageProps) {
</div>
</div>
<div className="flex min-h-0 flex-1 gap-6">
<div className="scrollbar-subtle min-w-0 flex-1 space-y-5 overflow-y-auto pr-1">
<div className="flex min-h-0 flex-1 gap-4">
<div className="scrollbar-subtle min-w-0 flex-1 space-y-3 overflow-y-auto pr-1">
<SectionCard
icon={<Boxes size={18} />}
icon={<Boxes size={15} />}
title="Dify 应用配置"
description="从「模型资源」中选择 Dify 应用。开场白、知识库、提示词等对话编排请在 Dify 平台配置,本页不重复设置。"
>
@@ -1525,7 +1552,7 @@ export function AssistantPage(props: AssistantPageProps) {
</SectionCard>
<SectionCard
icon={<Brain size={18} />}
icon={<Brain size={15} />}
title="语音配置"
description="从「模型资源」中选择语音识别与语音合成。大模型、知识库与开场白由 Dify 应用提供,请前往 Dify 平台配置。"
>
@@ -1546,7 +1573,7 @@ export function AssistantPage(props: AssistantPageProps) {
</SectionCard>
<SectionCard
icon={<Sparkles size={18} />}
icon={<Sparkles size={15} />}
title="交互策略"
description="设置实时视频对话时的交互体验"
>
@@ -1604,10 +1631,10 @@ export function AssistantPage(props: AssistantPageProps) {
</div>
</div>
<div className="flex min-h-0 flex-1 gap-6">
<div className="scrollbar-subtle min-w-0 flex-1 space-y-5 overflow-y-auto pr-1">
<div className="flex min-h-0 flex-1 gap-4">
<div className="scrollbar-subtle min-w-0 flex-1 space-y-3 overflow-y-auto pr-1">
<SectionCard
icon={<Database size={18} />}
icon={<Database size={15} />}
title="FastGPT 应用配置"
description="从「模型资源」中选择 FastGPT 应用。开场白、知识库、提示词等对话编排请在 FastGPT 平台配置,本页不重复设置。"
>
@@ -1621,7 +1648,7 @@ export function AssistantPage(props: AssistantPageProps) {
</SectionCard>
<SectionCard
icon={<Brain size={18} />}
icon={<Brain size={15} />}
title="语音配置"
description="从「模型资源」中选择语音识别与语音合成。大模型、知识库与开场白由 FastGPT 应用提供,请前往 FastGPT 平台配置。"
>
@@ -1642,7 +1669,7 @@ export function AssistantPage(props: AssistantPageProps) {
</SectionCard>
<SectionCard
icon={<Sparkles size={18} />}
icon={<Sparkles size={15} />}
title="交互策略"
description="设置实时视频对话时的交互体验"
>
@@ -1704,10 +1731,10 @@ export function AssistantPage(props: AssistantPageProps) {
</div>
</div>
<div className="flex min-h-0 flex-1 gap-6">
<div className="scrollbar-subtle min-w-0 flex-1 space-y-5 overflow-y-auto pr-1">
<div className="flex min-h-0 flex-1 gap-4">
<div className="scrollbar-subtle min-w-0 flex-1 space-y-3 overflow-y-auto pr-1">
<SectionCard
icon={<Terminal size={18} />}
icon={<Terminal size={15} />}
title="OpenCode 服务配置"
description="从「模型资源」中选择 OpenCode 服务资源。"
>
@@ -1721,7 +1748,7 @@ export function AssistantPage(props: AssistantPageProps) {
</SectionCard>
<SectionCard
icon={<MessageSquareText size={18} />}
icon={<MessageSquareText size={15} />}
title="提示词"
description="描述助手的角色、能力和回答要求"
>
@@ -1734,7 +1761,7 @@ export function AssistantPage(props: AssistantPageProps) {
</SectionCard>
<SectionCard
icon={<Brain size={18} />}
icon={<Brain size={15} />}
title="模型与语音配置"
description="配置 OpenCode 使用的大语言模型、语音识别与语音合成资源。"
>
@@ -1779,7 +1806,7 @@ export function AssistantPage(props: AssistantPageProps) {
</SectionCard>
<SectionCard
icon={<Sparkles size={18} />}
icon={<Sparkles size={15} />}
title="交互策略"
description="设置实时视频对话时的交互体验"
>
@@ -1843,10 +1870,10 @@ export function AssistantPage(props: AssistantPageProps) {
}
/>
<div className="flex min-h-0 flex-1 gap-6">
<div className="scrollbar-subtle min-w-0 flex-1 space-y-5 overflow-y-auto pr-1">
<div className="flex min-h-0 flex-1 gap-4">
<div className="scrollbar-subtle min-w-0 flex-1 space-y-3 overflow-y-auto pr-1">
<SectionCard>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
<div
role="button"
tabIndex={0}
@@ -1858,25 +1885,25 @@ export function AssistantPage(props: AssistantPageProps) {
}
}}
className={[
"cursor-pointer rounded-2xl border p-5 text-left transition-colors",
"cursor-pointer rounded-xl border p-3.5 text-left transition-colors",
form.runtimeMode === "pipeline"
? "border-primary bg-primary/5 ring-1 ring-primary"
: "border-hairline bg-canvas-soft hover:border-hairline-strong",
].join(" ")}
>
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-surface-strong text-foreground">
<Waypoints size={18} />
<div className="flex items-center gap-2.5">
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-surface-strong text-foreground">
<Waypoints size={15} />
</div>
<div className="flex items-center gap-1.5">
<span className="font-medium text-foreground">Pipeline </span>
<span className="text-sm font-medium text-foreground">Pipeline </span>
<HelpHint text="通过 ASR、LLM 和 TTS 级联组成语音管线,灵活选配各模块。" />
</div>
</div>
{form.runtimeMode === "pipeline" && (
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground">
<Check size={14} />
<span className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground">
<Check size={12} />
</span>
)}
</div>
@@ -1893,25 +1920,25 @@ export function AssistantPage(props: AssistantPageProps) {
}
}}
className={[
"cursor-pointer rounded-2xl border p-5 text-left transition-colors",
"cursor-pointer rounded-xl border p-3.5 text-left transition-colors",
form.runtimeMode === "realtime"
? "border-primary bg-primary/5 ring-1 ring-primary"
: "border-hairline bg-canvas-soft hover:border-hairline-strong",
].join(" ")}
>
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-surface-strong text-foreground">
<AudioLines size={18} />
<div className="flex items-center gap-2.5">
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-surface-strong text-foreground">
<AudioLines size={15} />
</div>
<div className="flex items-center gap-1.5">
<span className="font-medium text-foreground">Realtime </span>
<span className="text-sm font-medium text-foreground">Realtime </span>
<HelpHint text="使用原生实时语音模型,模型直接处理音频输入并生成语音回复。" />
</div>
</div>
{form.runtimeMode === "realtime" && (
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground">
<Check size={14} />
<span className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground">
<Check size={12} />
</span>
)}
</div>
@@ -1920,7 +1947,7 @@ export function AssistantPage(props: AssistantPageProps) {
</SectionCard>
<SectionCard
icon={<MessageSquareText size={18} />}
icon={<MessageSquareText size={15} />}
title="提示词"
description="描述助手的角色、能力和回答要求"
>
@@ -1938,7 +1965,7 @@ export function AssistantPage(props: AssistantPageProps) {
{form.runtimeMode === "pipeline" ? (
<SectionCard
icon={<Brain size={18} />}
icon={<Brain size={15} />}
title="模型配置"
description="从「模型资源」中选择大语言模型、语音识别与语音合成"
>
@@ -1983,7 +2010,7 @@ export function AssistantPage(props: AssistantPageProps) {
</SectionCard>
) : (
<SectionCard
icon={<Brain size={18} />}
icon={<Brain size={15} />}
title="模型配置"
description="当前模式下 ASR 与 TTS 由 Realtime 模型内置完成"
>
@@ -1998,7 +2025,7 @@ export function AssistantPage(props: AssistantPageProps) {
)}
<SectionCard
icon={<Bot size={18} />}
icon={<Bot size={15} />}
title="开场白"
description="助手与用户首次对话时的开场语"
>
@@ -2015,7 +2042,7 @@ export function AssistantPage(props: AssistantPageProps) {
{form.runtimeMode === "pipeline" && (
<SectionCard
icon={<Database size={18} />}
icon={<Database size={15} />}
title="知识库配置"
description="选择助手回答时可检索的业务知识来源"
>
@@ -2041,7 +2068,7 @@ export function AssistantPage(props: AssistantPageProps) {
)}
<SectionCard
icon={<Wrench size={18} />}
icon={<Wrench size={15} />}
title="工具"
description="配置该提示词助手可以调用的工具"
>
@@ -2053,7 +2080,7 @@ export function AssistantPage(props: AssistantPageProps) {
</SectionCard>
<SectionCard
icon={<Sparkles size={18} />}
icon={<Sparkles size={15} />}
title="交互策略"
description="设置实时视频对话时的交互体验"
>
@@ -3377,29 +3404,6 @@ function EditableTitle({
);
}
function HelpHint({ text }: { text: string }) {
return (
<Popover>
<PopoverTrigger asChild>
<button
type="button"
aria-label="查看说明"
onClick={(event) => event.stopPropagation()}
className="flex h-5 w-5 items-center justify-center rounded-full text-muted-soft transition-colors hover:bg-surface-strong hover:text-foreground"
>
<HelpCircle size={14} />
</button>
</PopoverTrigger>
<PopoverContent
align="start"
className="w-72 text-sm leading-6 text-muted-foreground"
>
{text}
</PopoverContent>
</Popover>
);
}
function DynamicVariableEditorHint({
count,
onOpen,
@@ -3657,45 +3661,6 @@ function DynamicVariablesDialog({
);
}
function SectionCard({
icon,
title,
description,
children,
}: {
icon?: React.ReactNode;
title?: string;
description?: string;
children: React.ReactNode;
}) {
const hasHeader = Boolean(title);
return (
<Card className="rounded-2xl border-hairline bg-card text-card-foreground shadow-sm">
{hasHeader && (
<CardHeader>
<div className="flex items-center gap-3">
{icon && (
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-surface-strong text-foreground">
{icon}
</div>
)}
<div className="flex items-center gap-1.5">
<CardTitle className="text-base font-medium">{title}</CardTitle>
{description && <HelpHint text={description} />}
</div>
</div>
</CardHeader>
)}
<CardContent className={hasHeader ? "space-y-4" : undefined}>
{children}
</CardContent>
</Card>
);
}
function TextAreaField({
label,
value,
@@ -3712,7 +3677,7 @@ function TextAreaField({
return (
<label className="block">
{label && (
<div className="mb-2 text-sm font-medium text-foreground">{label}</div>
<div className="mb-1.5 text-sm font-medium text-foreground">{label}</div>
)}
<Textarea
value={value}
@@ -3721,7 +3686,7 @@ function TextAreaField({
rows={rows}
// Override ui/textarea's field-sizing-content so `rows` sets a real height
// instead of collapsing to min-h-16 when the value is short.
className="field-sizing-fixed min-h-32 resize-y border-hairline-strong bg-background text-foreground placeholder:text-muted-soft"
className="field-sizing-fixed min-h-28 resize-y border-hairline-strong bg-background text-sm text-foreground placeholder:text-muted-soft"
/>
</label>
);
@@ -3747,7 +3712,7 @@ function ResourceSelectField({
return (
<div className="block">
{label && (
<div className="mb-2 text-sm font-medium text-foreground">{label}</div>
<div className="mb-1.5 text-sm font-medium text-foreground">{label}</div>
)}
<Select
@@ -4056,18 +4021,18 @@ function ToggleRow({
<div
className={[
"flex items-center justify-between border border-hairline bg-canvas-soft",
hasIcon ? "rounded-2xl p-5" : "rounded-xl p-4",
hasIcon ? "rounded-xl p-3.5" : "rounded-xl px-3.5 py-3",
].join(" ")}
>
<div>
<div
className={[
"flex items-center font-medium text-foreground",
hasIcon ? "gap-3" : "gap-1.5",
"flex items-center text-sm font-medium text-foreground",
hasIcon ? "gap-2.5" : "gap-1.5",
].join(" ")}
>
{icon && (
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-surface-strong text-foreground">
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-surface-strong text-foreground">
{icon}
</span>
)}
@@ -4077,7 +4042,7 @@ function ToggleRow({
</span>
</div>
{description && (
<div className="mt-1 text-sm text-muted-foreground">
<div className="mt-1 text-xs text-muted-foreground">
{description}
</div>
)}

View File

@@ -1,8 +1,9 @@
"use client";
import type { ReactNode } from "react";
import { HelpCircle, Settings2 } from "lucide-react";
import { Settings2 } from "lucide-react";
import { HelpHint } from "@/components/editor/section-card";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import {
@@ -13,11 +14,6 @@ import {
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
Select,
SelectContent,
@@ -59,7 +55,7 @@ export function TurnConfigEditor({
});
return (
<div className="flex items-center justify-between gap-4 rounded-2xl border border-hairline bg-card p-4 shadow-sm">
<div className="flex items-center justify-between gap-3 rounded-xl border border-hairline bg-canvas-soft px-3.5 py-3">
<div className="flex items-center gap-1.5">
<span className="text-sm font-medium text-foreground">
@@ -72,7 +68,7 @@ export function TurnConfigEditor({
aria-label="打开允许用户打断高级配置"
className="flex h-5 w-5 items-center justify-center rounded-full text-muted-soft transition-colors hover:bg-surface-strong hover:text-foreground"
>
<Settings2 size={14} />
<Settings2 size={13} />
</button>
</DialogTrigger>
<DialogContent className="max-h-[calc(100vh-3rem)] overflow-y-auto sm:max-w-6xl lg:max-w-[88rem] lg:overflow-hidden">
@@ -161,29 +157,6 @@ function ConfigSection({ title, children }: { title: string; children: ReactNode
);
}
function HelpHint({ text }: { text: string }) {
return (
<Popover>
<PopoverTrigger asChild>
<button
type="button"
aria-label="查看允许用户打断说明"
onClick={(event) => event.stopPropagation()}
className="flex h-5 w-5 items-center justify-center rounded-full text-muted-soft transition-colors hover:bg-surface-strong hover:text-foreground"
>
<HelpCircle size={14} />
</button>
</PopoverTrigger>
<PopoverContent
align="start"
className="w-72 text-sm leading-6 text-muted-foreground"
>
{text}
</PopoverContent>
</Popover>
);
}
function NumberField({ label, value, min, max, step, onChange }: { label: string; value: number; min: number; max: number; step: number; onChange: (value: number) => void }) {
return (
<label className="block space-y-2">

View File

@@ -30,14 +30,24 @@ export function GenericNode({ id, type, data, selected }: NodeProps) {
const preview = (nodeData.greeting || nodeData.prompt || nodeData.message || "")
.toString()
.trim();
const entryModeLabel = {
wait_user: "等待用户",
generate: "立即回复",
fixed_speech: "固定进入语",
}[nodeData.entryMode ?? "wait_user"];
const inheritsGlobal = nodeData.inheritGlobalConfig !== false;
const meta = type === "agent"
? [
nodeData.contextPolicy === "fresh" ? "独立上下文" : "继承上下文",
`${nodeData.toolIds?.length ?? 0} 工具`,
nodeData.knowledgeBaseId ? "知识库" : null,
nodeData.asrResourceId ? "独立 ASR" : null,
nodeData.ttsResourceId ? "独立 TTS" : null,
].filter(Boolean)
? inheritsGlobal
? [entryModeLabel, "继承全局配置"]
: [
entryModeLabel,
"自定义配置",
nodeData.llmResourceId ? "独立 LLM" : null,
`${nodeData.toolIds?.length ?? 0} 工具`,
nodeData.knowledgeBaseId ? "知识库" : null,
nodeData.asrResourceId ? "独立 ASR" : null,
nodeData.ttsResourceId ? "独立 TTS" : null,
].filter(Boolean)
: type === "action" && nodeData.toolId
? ["确定性工具"]
: [];

File diff suppressed because it is too large Load Diff

View File

@@ -8,6 +8,7 @@ import type { NodeSpecDto } from "@/lib/api";
export type WorkflowNodeType = "start" | "agent" | "action" | "handoff" | "end";
export type ContextPolicy = "inherit" | "fresh";
export type KnowledgeMode = "automatic" | "on_demand" | "disabled";
export type AgentEntryMode = "wait_user" | "generate" | "fixed_speech";
export type EdgeMode = "llm" | "expression" | "always";
export type ExpressionOperator =
| "eq"
@@ -25,11 +26,15 @@ export type WorkflowNodeData = {
greeting?: string;
prompt?: string;
contextPolicy?: ContextPolicy;
inheritGlobalConfig?: boolean;
entryMode?: AgentEntryMode;
entrySpeech?: string;
toolIds?: string[];
knowledgeBaseId?: string;
knowledgeMode?: KnowledgeMode;
knowledgeTopN?: number;
knowledgeScoreThreshold?: number;
llmResourceId?: string;
asrResourceId?: string;
ttsResourceId?: string;
toolId?: string;
@@ -129,8 +134,14 @@ export type WorkflowGraph = {
specVersion: 3;
settings: {
globalPrompt: string;
defaultLlmResourceId: string;
defaultAsrResourceId: string;
defaultTtsResourceId: string;
toolIds: string[];
knowledgeBaseId: string;
knowledgeMode: "automatic" | "on_demand";
knowledgeTopN: number;
knowledgeScoreThreshold: number;
};
nodes: Array<{
id: string;
@@ -153,8 +164,14 @@ export function defaultGraph(): WorkflowGraph {
settings: {
globalPrompt:
"你是一个友好、专业的语音助手。请使用简短、自然、适合口语表达的句子。",
defaultLlmResourceId: "",
defaultAsrResourceId: "",
defaultTtsResourceId: "",
toolIds: [],
knowledgeBaseId: "",
knowledgeMode: "automatic",
knowledgeTopN: 5,
knowledgeScoreThreshold: 0,
},
nodes: [
{
@@ -174,10 +191,9 @@ export function defaultGraph(): WorkflowGraph {
name: "Agent",
prompt: "了解用户需求并提供清晰、准确的帮助。",
contextPolicy: "inherit",
toolIds: [],
knowledgeMode: "disabled",
knowledgeTopN: 5,
knowledgeScoreThreshold: 0,
inheritGlobalConfig: true,
entryMode: "wait_user",
entrySpeech: "",
},
},
{