refactor: unify system tools as resources
This commit is contained in:
@@ -8,7 +8,6 @@ import {
|
||||
ChevronLeft,
|
||||
Copy,
|
||||
Pencil,
|
||||
PhoneOff,
|
||||
Plus,
|
||||
ServerCog,
|
||||
Settings2,
|
||||
@@ -19,7 +18,6 @@ import {
|
||||
} from "lucide-react";
|
||||
|
||||
import { HelpHint } from "@/components/editor/section-card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -47,10 +45,8 @@ import {
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import type {
|
||||
KnowledgeRetrievalConfig,
|
||||
SystemToolKind,
|
||||
Tool,
|
||||
} from "@/lib/api";
|
||||
import { SYSTEM_TOOL_OPTIONS } from "@/lib/system-tools";
|
||||
|
||||
import type { RuntimeMode } from "./types";
|
||||
|
||||
@@ -487,34 +483,20 @@ export function ToolPicker({
|
||||
tools,
|
||||
selectedIds,
|
||||
onChange,
|
||||
selectedSystemTools,
|
||||
onSystemToolsChange,
|
||||
showBuiltInSystemTools,
|
||||
}: {
|
||||
tools: Tool[];
|
||||
selectedIds: string[];
|
||||
onChange: (ids: string[]) => void;
|
||||
selectedSystemTools: SystemToolKind[];
|
||||
onSystemToolsChange: (tools: SystemToolKind[]) => void;
|
||||
showBuiltInSystemTools: boolean;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<Tool["type"]>("system");
|
||||
const [draftIds, setDraftIds] = useState<string[]>(selectedIds);
|
||||
const [draftSystemTools, setDraftSystemTools] =
|
||||
useState<SystemToolKind[]>(selectedSystemTools);
|
||||
const selectedTools = selectedIds
|
||||
.map((id) => tools.find((tool) => tool.id === id))
|
||||
.filter((tool): tool is Tool => Boolean(tool));
|
||||
const selectedBuiltInTools = selectedSystemTools
|
||||
.map((kind) => SYSTEM_TOOL_OPTIONS.find((option) => option.value === kind))
|
||||
.filter((option): option is (typeof SYSTEM_TOOL_OPTIONS)[number] =>
|
||||
Boolean(option),
|
||||
);
|
||||
|
||||
function openPicker() {
|
||||
setDraftIds(selectedIds);
|
||||
setDraftSystemTools(selectedSystemTools);
|
||||
setOpen(true);
|
||||
}
|
||||
|
||||
@@ -526,14 +508,6 @@ export function ToolPicker({
|
||||
);
|
||||
}
|
||||
|
||||
function toggleBuiltInTool(kind: SystemToolKind) {
|
||||
setDraftSystemTools((current) =>
|
||||
current.includes(kind)
|
||||
? current.filter((item) => item !== kind)
|
||||
: [...current, kind],
|
||||
);
|
||||
}
|
||||
|
||||
const tabs: Array<{ value: Tool["type"]; label: string }> = [
|
||||
{ value: "system", label: "System" },
|
||||
{ value: "http", label: "HTTP" },
|
||||
@@ -544,34 +518,13 @@ export function ToolPicker({
|
||||
return (
|
||||
<>
|
||||
<div className="flex min-h-9 flex-wrap items-center gap-2">
|
||||
{selectedBuiltInTools.map((tool) => (
|
||||
<div
|
||||
key={`built-in-${tool.value}`}
|
||||
className="flex h-8 items-center gap-2 rounded-lg border border-hairline-strong bg-background px-2.5 text-sm"
|
||||
>
|
||||
<Sparkles size={14} />
|
||||
<span className="max-w-48 truncate">{tool.label}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
onSystemToolsChange(
|
||||
selectedSystemTools.filter((kind) => kind !== tool.value),
|
||||
)
|
||||
}
|
||||
className="text-muted-soft transition-colors hover:text-foreground"
|
||||
aria-label={`移除系统工具 ${tool.label}`}
|
||||
>
|
||||
<X size={13} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
{selectedTools.map((tool) => (
|
||||
<div
|
||||
key={tool.id}
|
||||
className="flex h-8 items-center gap-2 rounded-lg border border-hairline-strong bg-background px-2.5 text-sm"
|
||||
>
|
||||
{tool.type === "system" ? (
|
||||
<PhoneOff size={14} />
|
||||
<Sparkles size={14} />
|
||||
) : tool.type === "mcp" ? (
|
||||
<ServerCog size={14} />
|
||||
) : (
|
||||
@@ -606,7 +559,7 @@ export function ToolPicker({
|
||||
<DialogHeader>
|
||||
<DialogTitle>选择工具</DialogTitle>
|
||||
<DialogDescription>
|
||||
按类型选择内置系统工具或已经启用的工具资源。
|
||||
按类型选择已经启用的工具资源。
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -631,9 +584,7 @@ export function ToolPicker({
|
||||
|
||||
{tabs.map((tab) => {
|
||||
const resources = tools.filter((tool) => tool.type === tab.value);
|
||||
const hasBuiltIns =
|
||||
tab.value === "system" && showBuiltInSystemTools;
|
||||
const isEmpty = resources.length === 0 && !hasBuiltIns;
|
||||
const isEmpty = resources.length === 0;
|
||||
|
||||
return (
|
||||
<TabsContent key={tab.value} value={tab.value} className="pt-3">
|
||||
@@ -643,35 +594,6 @@ export function ToolPicker({
|
||||
</div>
|
||||
) : (
|
||||
<div className="max-h-[280px] divide-y divide-hairline overflow-y-auto rounded-xl border border-hairline">
|
||||
{hasBuiltIns &&
|
||||
SYSTEM_TOOL_OPTIONS.map((option) => {
|
||||
const checked = draftSystemTools.includes(option.value);
|
||||
return (
|
||||
<label
|
||||
key={`built-in-${option.value}`}
|
||||
className="flex h-14 cursor-pointer items-center gap-3 px-4 transition-colors hover:bg-surface-strong/40"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
onChange={() => toggleBuiltInTool(option.value)}
|
||||
className="size-4 accent-primary"
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="truncate font-medium text-foreground">
|
||||
{option.label}
|
||||
</span>
|
||||
<Badge variant="secondary">内置</Badge>
|
||||
</div>
|
||||
<div className="mt-0.5 truncate text-xs text-muted-foreground">
|
||||
{option.description}
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
|
||||
{resources.map((tool) => {
|
||||
const checked = draftIds.includes(tool.id);
|
||||
return (
|
||||
@@ -686,13 +608,8 @@ export function ToolPicker({
|
||||
className="size-4 accent-primary"
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="truncate font-medium text-foreground">
|
||||
{tool.name}
|
||||
</span>
|
||||
{tool.type === "system" && (
|
||||
<Badge variant="secondary">资源</Badge>
|
||||
)}
|
||||
<div className="truncate font-medium text-foreground">
|
||||
{tool.name}
|
||||
</div>
|
||||
<div className="mt-0.5 truncate font-mono text-xs text-muted-foreground">
|
||||
{tool.functionName}
|
||||
@@ -715,7 +632,6 @@ export function ToolPicker({
|
||||
<Button
|
||||
onClick={() => {
|
||||
onChange(draftIds);
|
||||
onSystemToolsChange(draftSystemTools);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -506,8 +506,15 @@ export function PromptEditor({
|
||||
openingMessage: null,
|
||||
});
|
||||
}
|
||||
if (runtimeMode === "realtime" && form.systemTools.length) {
|
||||
updateForm("systemTools", []);
|
||||
if (runtimeMode === "realtime") {
|
||||
updateForm(
|
||||
"toolIds",
|
||||
form.toolIds.filter(
|
||||
(id) =>
|
||||
tools.find((tool) => tool.id === id)?.type !==
|
||||
"system",
|
||||
),
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -602,14 +609,13 @@ export function PromptEditor({
|
||||
description="配置该提示词助手可以调用的工具"
|
||||
>
|
||||
<ToolPicker
|
||||
tools={tools.filter((tool) => tool.status === "active")}
|
||||
tools={tools.filter(
|
||||
(tool) =>
|
||||
tool.status === "active" &&
|
||||
(form.runtimeMode === "pipeline" || tool.type !== "system"),
|
||||
)}
|
||||
selectedIds={form.toolIds}
|
||||
onChange={(toolIds) => updateForm("toolIds", toolIds)}
|
||||
selectedSystemTools={form.systemTools}
|
||||
onSystemToolsChange={(systemTools) =>
|
||||
updateForm("systemTools", systemTools)
|
||||
}
|
||||
showBuiltInSystemTools={form.runtimeMode === "pipeline"}
|
||||
/>
|
||||
</SectionCard>
|
||||
</section>
|
||||
|
||||
@@ -2,7 +2,6 @@ import type {
|
||||
DynamicVariableDefinition,
|
||||
KnowledgeRetrievalConfig,
|
||||
StartupConfig,
|
||||
SystemToolKind,
|
||||
TurnConfig,
|
||||
} from "@/lib/api";
|
||||
|
||||
@@ -23,7 +22,6 @@ export type AssistantForm = {
|
||||
enableInterrupt: boolean;
|
||||
turnConfig: TurnConfig;
|
||||
startup: StartupConfig;
|
||||
systemTools: SystemToolKind[];
|
||||
visionEnabled: boolean;
|
||||
visionModelResourceId: string;
|
||||
toolIds: string[];
|
||||
|
||||
@@ -184,7 +184,6 @@ function blankPromptForm(name: string): AssistantForm {
|
||||
actions: [],
|
||||
openingMessage: null,
|
||||
},
|
||||
systemTools: [],
|
||||
visionEnabled: false,
|
||||
visionModelResourceId: "",
|
||||
toolIds: [],
|
||||
@@ -480,7 +479,6 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
actions: a.startup?.actions ?? [],
|
||||
openingMessage: a.startup?.openingMessage ?? null,
|
||||
},
|
||||
systemTools: a.systemTools ?? [],
|
||||
visionEnabled: a.visionEnabled,
|
||||
visionModelResourceId: a.visionModelResourceId ?? "",
|
||||
toolIds: a.toolIds ?? [],
|
||||
@@ -557,7 +555,6 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
actions: [],
|
||||
openingMessage: null,
|
||||
},
|
||||
systemTools: [],
|
||||
visionEnabled: false,
|
||||
visionModelResourceId: null,
|
||||
modelResourceIds: {},
|
||||
@@ -617,7 +614,6 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
enableInterrupt: form.enableInterrupt,
|
||||
turnConfig: form.turnConfig,
|
||||
startup: form.startup,
|
||||
systemTools: form.runtimeMode === "pipeline" ? form.systemTools : [],
|
||||
visionEnabled: form.visionEnabled,
|
||||
visionModelResourceId: form.visionModelResourceId || null,
|
||||
modelResourceIds: {
|
||||
@@ -1304,10 +1300,16 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
vision: visionModelOptionsFor(""),
|
||||
}}
|
||||
toolOptions={tools
|
||||
.filter(
|
||||
(tool) => tool.status === "active" && tool.type !== "system",
|
||||
)
|
||||
.map((tool) => ({ value: tool.id, label: tool.name }))}
|
||||
.filter((tool) => tool.status === "active")
|
||||
.map((tool) => ({
|
||||
value: tool.id,
|
||||
label: tool.name,
|
||||
toolType: tool.type,
|
||||
systemKind:
|
||||
tool.definition.type === "system"
|
||||
? tool.definition.config.kind
|
||||
: undefined,
|
||||
}))}
|
||||
knowledgeOptions={kbOptions}
|
||||
onBack={() => router.push("/assistants")}
|
||||
onSave={() => void handleSaveWorkflow()}
|
||||
|
||||
@@ -57,12 +57,14 @@ import {
|
||||
type ClientToolResponseWaitMode,
|
||||
type HttpToolDefinition,
|
||||
type McpServer,
|
||||
type SystemToolKind,
|
||||
type Tool,
|
||||
type ToolParameter,
|
||||
type ToolExecutionMode,
|
||||
type ToolStatus,
|
||||
type ToolUpsert,
|
||||
} from "@/lib/api";
|
||||
import { SYSTEM_TOOL_OPTIONS } from "@/lib/system-tools";
|
||||
|
||||
type ToolKind = "system" | "http" | "client";
|
||||
type HttpMethod = HttpToolDefinition["config"]["method"];
|
||||
@@ -86,6 +88,7 @@ type ToolForm = {
|
||||
type: ToolKind;
|
||||
description: string;
|
||||
status: ToolStatus;
|
||||
systemKind: SystemToolKind;
|
||||
messageType: "none" | "custom";
|
||||
customMessage: string;
|
||||
captureReason: boolean;
|
||||
@@ -115,6 +118,7 @@ function blankForm(): ToolForm {
|
||||
type: "system",
|
||||
description: "",
|
||||
status: "active",
|
||||
systemKind: "end_conversation",
|
||||
messageType: "none",
|
||||
customMessage: "",
|
||||
captureReason: true,
|
||||
@@ -148,6 +152,7 @@ function formFromTool(tool: Tool): ToolForm {
|
||||
base.description = tool.description;
|
||||
base.status = tool.status;
|
||||
if (tool.definition.type === "system") {
|
||||
base.systemKind = tool.definition.config.kind;
|
||||
base.messageType = tool.definition.config.messageType;
|
||||
base.customMessage = tool.definition.config.customMessage;
|
||||
base.captureReason = tool.definition.config.captureReason;
|
||||
@@ -246,9 +251,13 @@ function payloadFromForm(form: ToolForm): ToolUpsert {
|
||||
schemaVersion: 1,
|
||||
type: "system",
|
||||
config: {
|
||||
kind: "end_conversation",
|
||||
kind: form.systemKind,
|
||||
messageType: form.messageType,
|
||||
customMessage: form.messageType === "custom" ? form.customMessage : "",
|
||||
customMessage:
|
||||
form.systemKind === "end_conversation" &&
|
||||
form.messageType === "custom"
|
||||
? form.customMessage
|
||||
: "",
|
||||
captureReason: form.captureReason,
|
||||
},
|
||||
},
|
||||
@@ -871,52 +880,80 @@ function SystemToolFields({
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Field label="系统动作">
|
||||
<Select value="end_conversation" disabled>
|
||||
<Select
|
||||
value={form.systemKind}
|
||||
onValueChange={(systemKind: SystemToolKind) =>
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
systemKind,
|
||||
functionName: systemKind,
|
||||
}))
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-full border-hairline-strong bg-background">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="end_conversation">结束对话</SelectItem>
|
||||
{SYSTEM_TOOL_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
<Field label="结束语">
|
||||
<Select
|
||||
value={form.messageType}
|
||||
onValueChange={(messageType: "none" | "custom") =>
|
||||
setForm((current) => ({ ...current, messageType }))
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-full border-hairline-strong bg-background"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">不播放固定结束语</SelectItem>
|
||||
<SelectItem value="custom">自定义结束语</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
{form.messageType === "custom" && (
|
||||
<Field label="自定义结束语">
|
||||
<Textarea
|
||||
value={form.customMessage}
|
||||
onChange={(event) =>
|
||||
setForm((current) => ({ ...current, customMessage: event.target.value }))
|
||||
}
|
||||
rows={3}
|
||||
/>
|
||||
</Field>
|
||||
<p className="text-xs leading-5 text-muted-foreground">
|
||||
{SYSTEM_TOOL_OPTIONS.find((option) => option.value === form.systemKind)
|
||||
?.description ?? "由平台执行的会话控制能力。"}
|
||||
</p>
|
||||
{form.systemKind === "end_conversation" && (
|
||||
<>
|
||||
<Field label="结束语">
|
||||
<Select
|
||||
value={form.messageType}
|
||||
onValueChange={(messageType: "none" | "custom") =>
|
||||
setForm((current) => ({ ...current, messageType }))
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-full border-hairline-strong bg-background">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">不播放固定结束语</SelectItem>
|
||||
<SelectItem value="custom">自定义结束语</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
{form.messageType === "custom" && (
|
||||
<Field label="自定义结束语">
|
||||
<Textarea
|
||||
value={form.customMessage}
|
||||
onChange={(event) =>
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
customMessage: event.target.value,
|
||||
}))
|
||||
}
|
||||
rows={3}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
<div className="flex items-center justify-between gap-4 rounded-lg border border-hairline-strong px-4 py-3">
|
||||
<div>
|
||||
<div className="font-medium text-foreground">记录结束原因</div>
|
||||
<div className="mt-0.5 text-xs text-muted-foreground">
|
||||
要求模型在调用时提供 reason 参数
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
checked={form.captureReason}
|
||||
onCheckedChange={(captureReason) =>
|
||||
setForm((current) => ({ ...current, captureReason }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="flex items-center justify-between gap-4 rounded-lg border border-hairline-strong px-4 py-3">
|
||||
<div>
|
||||
<div className="font-medium text-foreground">记录结束原因</div>
|
||||
<div className="mt-0.5 text-xs text-muted-foreground">要求模型在调用时提供 reason 参数</div>
|
||||
</div>
|
||||
<Switch
|
||||
checked={form.captureReason}
|
||||
onCheckedChange={(captureReason) =>
|
||||
setForm((current) => ({ ...current, captureReason }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -72,7 +72,6 @@ function defaultNodeData(spec: RuntimeNodeSpec): WorkflowNodeData {
|
||||
contextPolicy: "inherit",
|
||||
inheritGlobalConfig: true,
|
||||
entryMode: "wait_user",
|
||||
systemTools: [],
|
||||
stateVariableNames: [],
|
||||
});
|
||||
} else if (spec.type === "action") {
|
||||
|
||||
@@ -48,7 +48,7 @@ export function ActionNodePanel({
|
||||
<NodeSelect
|
||||
label="执行工具"
|
||||
value={(draft.toolId as string) || ""}
|
||||
options={toolOptions}
|
||||
options={toolOptions.filter((option) => option.toolType !== "system")}
|
||||
onChange={(value) => set("toolId", value || "")}
|
||||
noneLabel="请选择工具"
|
||||
/>
|
||||
|
||||
@@ -16,8 +16,7 @@ import { VisionConfigSection } from "@/components/editor/vision-config-section";
|
||||
import { TurnConfigEditor } from "@/components/turn-config-editor";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import type { KnowledgeRetrievalConfig, SystemToolKind } from "@/lib/api";
|
||||
import { SYSTEM_TOOL_OPTIONS } from "@/lib/system-tools";
|
||||
import type { KnowledgeRetrievalConfig } from "@/lib/api";
|
||||
import { normalizeTurnConfig } from "@/lib/turn-config";
|
||||
|
||||
import { NodeSelect, ToolOptionPicker } from "./controls";
|
||||
@@ -104,18 +103,14 @@ export function AgentNodePanel({
|
||||
turnConfig: agentTurnConfig,
|
||||
});
|
||||
};
|
||||
const toggleSystemTool = (kind: SystemToolKind, enabled: boolean) => {
|
||||
const current = draft.systemTools ?? [];
|
||||
const systemTools = enabled
|
||||
? [...new Set([...current, kind])]
|
||||
: current.filter((item) => item !== kind);
|
||||
setPatch({
|
||||
systemTools,
|
||||
...(!enabled && kind === "update_state"
|
||||
? { stateVariableNames: [] }
|
||||
: {}),
|
||||
});
|
||||
};
|
||||
const selectedToolIds = inheritsGlobal
|
||||
? workflowSettings.toolIds
|
||||
: draft.toolIds ?? [];
|
||||
const updateStateEnabled = selectedToolIds.some(
|
||||
(toolId) =>
|
||||
toolOptions.find((option) => option.value === toolId)?.systemKind ===
|
||||
"update_state",
|
||||
);
|
||||
const toggleStateVariable = (name: string, enabled: boolean) => {
|
||||
const current = draft.stateVariableNames ?? [];
|
||||
set(
|
||||
@@ -133,7 +128,9 @@ export function AgentNodePanel({
|
||||
{ id: "scope", label: "配置范围" },
|
||||
{ id: "prompt", label: inheritsGlobal ? "任务" : "提示词" },
|
||||
{ id: "entry", label: "进入行为" },
|
||||
{ id: "system-tools", label: "系统工具" },
|
||||
...(updateStateEnabled
|
||||
? [{ id: "state-scope", label: "状态更新权限" }]
|
||||
: []),
|
||||
...(!inheritsGlobal
|
||||
? [
|
||||
{ id: "models", label: "模型与语音" },
|
||||
@@ -211,77 +208,43 @@ export function AgentNodePanel({
|
||||
</SectionCard>
|
||||
</PanelAnchor>
|
||||
|
||||
<PanelAnchor id="system-tools">
|
||||
<SectionCard
|
||||
icon={<Sparkles size={15} />}
|
||||
title="系统工具"
|
||||
description="只对当前 Agent 生效的内置会话控制能力"
|
||||
>
|
||||
<div className="space-y-3">
|
||||
{SYSTEM_TOOL_OPTIONS.map((option) => {
|
||||
const enabled = (draft.systemTools ?? []).includes(option.value);
|
||||
return (
|
||||
<div
|
||||
key={option.value}
|
||||
className="rounded-xl border border-hairline bg-canvas-soft p-3.5"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="min-w-0">
|
||||
<div className="text-sm font-medium text-foreground">
|
||||
{option.label}
|
||||
</div>
|
||||
<p className="mt-1 text-xs leading-5 text-muted-foreground">
|
||||
{option.description}
|
||||
</p>
|
||||
</div>
|
||||
{updateStateEnabled && (
|
||||
<PanelAnchor id="state-scope">
|
||||
<SectionCard
|
||||
icon={<Sparkles size={15} />}
|
||||
title="状态更新权限"
|
||||
description="更新状态工具只允许写入这里授权的动态变量"
|
||||
>
|
||||
{dynamicVariableOptions.length ? (
|
||||
<div className="space-y-2">
|
||||
{dynamicVariableOptions.map((variable) => (
|
||||
<div
|
||||
key={variable.value}
|
||||
className="flex items-center justify-between gap-3 rounded-lg border border-hairline bg-background px-3 py-2"
|
||||
>
|
||||
<span className="truncate text-xs text-foreground">
|
||||
{variable.label}
|
||||
</span>
|
||||
<Switch
|
||||
checked={enabled}
|
||||
checked={(draft.stateVariableNames ?? []).includes(
|
||||
variable.value,
|
||||
)}
|
||||
onCheckedChange={(checked) =>
|
||||
toggleSystemTool(option.value, checked)
|
||||
toggleStateVariable(variable.value, checked)
|
||||
}
|
||||
aria-label={`启用${option.label}`}
|
||||
aria-label={`允许更新${variable.value}`}
|
||||
/>
|
||||
</div>
|
||||
{option.value === "update_state" && enabled && (
|
||||
<div className="mt-3 border-t border-hairline pt-3">
|
||||
<div className="mb-2 text-xs font-medium text-foreground">
|
||||
允许更新的变量
|
||||
</div>
|
||||
{dynamicVariableOptions.length ? (
|
||||
<div className="space-y-2">
|
||||
{dynamicVariableOptions.map((variable) => (
|
||||
<div
|
||||
key={variable.value}
|
||||
className="flex items-center justify-between gap-3 rounded-lg border border-hairline bg-background px-3 py-2"
|
||||
>
|
||||
<span className="truncate text-xs text-foreground">
|
||||
{variable.label}
|
||||
</span>
|
||||
<Switch
|
||||
checked={(draft.stateVariableNames ?? []).includes(
|
||||
variable.value,
|
||||
)}
|
||||
onCheckedChange={(checked) =>
|
||||
toggleStateVariable(variable.value, checked)
|
||||
}
|
||||
aria-label={`允许更新${variable.value}`}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs leading-5 text-muted-foreground">
|
||||
请先在工作流的动态变量面板中声明变量。
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</SectionCard>
|
||||
</PanelAnchor>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs leading-5 text-muted-foreground">
|
||||
请先在工作流的动态变量面板中声明变量。
|
||||
</p>
|
||||
)}
|
||||
</SectionCard>
|
||||
</PanelAnchor>
|
||||
)}
|
||||
|
||||
{!inheritsGlobal && (
|
||||
<>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Plus, Wrench, X } from "lucide-react";
|
||||
import { Plus, ServerCog, Sparkles, Wrench, X } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -19,6 +19,13 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@/components/ui/tabs";
|
||||
import type { Tool } from "@/lib/api";
|
||||
|
||||
import type { ModelOption } from "../types";
|
||||
|
||||
@@ -70,10 +77,17 @@ export function ToolOptionPicker({
|
||||
onChange: (ids: string[]) => void;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<Tool["type"]>("system");
|
||||
const [draftIds, setDraftIds] = useState<string[]>(selectedIds);
|
||||
const selected = selectedIds
|
||||
.map((id) => options.find((option) => option.value === id))
|
||||
.filter((option): option is ModelOption => Boolean(option));
|
||||
const tabs: Array<{ value: Tool["type"]; label: string }> = [
|
||||
{ value: "system", label: "System" },
|
||||
{ value: "http", label: "HTTP" },
|
||||
{ value: "client", label: "Client" },
|
||||
{ value: "mcp", label: "MCP" },
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -83,7 +97,13 @@ export function ToolOptionPicker({
|
||||
key={option.value}
|
||||
className="flex h-8 items-center gap-2 rounded-lg border border-hairline-strong bg-background px-2.5 text-sm"
|
||||
>
|
||||
<Wrench size={14} />
|
||||
{option.toolType === "system" ? (
|
||||
<Sparkles size={14} />
|
||||
) : option.toolType === "mcp" ? (
|
||||
<ServerCog size={14} />
|
||||
) : (
|
||||
<Wrench size={14} />
|
||||
)}
|
||||
<span className="max-w-48 truncate">{option.label}</span>
|
||||
<button
|
||||
type="button"
|
||||
@@ -119,39 +139,69 @@ export function ToolOptionPicker({
|
||||
<DialogTitle>选择工具</DialogTitle>
|
||||
<DialogDescription>选择已经在工具资源中启用的工具。</DialogDescription>
|
||||
</DialogHeader>
|
||||
{options.length === 0 ? (
|
||||
<div className="rounded-xl border border-dashed border-hairline-strong px-4 py-10 text-center text-sm text-muted-foreground">
|
||||
暂无可用工具
|
||||
</div>
|
||||
) : (
|
||||
<div className="max-h-80 divide-y divide-hairline overflow-y-auto rounded-xl border border-hairline">
|
||||
{options.map((option) => {
|
||||
const checked = draftIds.includes(option.value);
|
||||
return (
|
||||
<label
|
||||
key={option.value}
|
||||
className="flex cursor-pointer items-center gap-3 px-4 py-3 transition-colors hover:bg-surface-strong/40"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
onChange={() =>
|
||||
setDraftIds((current) =>
|
||||
checked
|
||||
? current.filter((id) => id !== option.value)
|
||||
: [...current, option.value],
|
||||
)
|
||||
}
|
||||
className="size-4 accent-primary"
|
||||
/>
|
||||
<span className="truncate font-medium text-foreground">
|
||||
{option.label}
|
||||
</span>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onValueChange={(value) => setActiveTab(value as Tool["type"])}
|
||||
>
|
||||
<TabsList
|
||||
variant="line"
|
||||
className="w-full justify-start border-b border-hairline px-1"
|
||||
>
|
||||
{tabs.map((tab) => (
|
||||
<TabsTrigger
|
||||
key={tab.value}
|
||||
value={tab.value}
|
||||
className="flex-none px-4"
|
||||
>
|
||||
{tab.label}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
{tabs.map((tab) => {
|
||||
const rows = options.filter(
|
||||
(option) => (option.toolType ?? "http") === tab.value,
|
||||
);
|
||||
return (
|
||||
<TabsContent key={tab.value} value={tab.value} className="pt-3">
|
||||
{rows.length === 0 ? (
|
||||
<div className="rounded-xl border border-dashed border-hairline-strong px-4 py-10 text-center text-sm text-muted-foreground">
|
||||
暂无可用的 {tab.label} 工具
|
||||
</div>
|
||||
) : (
|
||||
<div className="max-h-[280px] divide-y divide-hairline overflow-y-auto rounded-xl border border-hairline">
|
||||
{rows.map((option) => {
|
||||
const checked = draftIds.includes(option.value);
|
||||
return (
|
||||
<label
|
||||
key={option.value}
|
||||
className="flex h-14 cursor-pointer items-center gap-3 px-4 transition-colors hover:bg-surface-strong/40"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
onChange={() =>
|
||||
setDraftIds((current) =>
|
||||
checked
|
||||
? current.filter(
|
||||
(id) => id !== option.value,
|
||||
)
|
||||
: [...current, option.value],
|
||||
)
|
||||
}
|
||||
className="size-4 accent-primary"
|
||||
/>
|
||||
<span className="truncate font-medium text-foreground">
|
||||
{option.label}
|
||||
</span>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
);
|
||||
})}
|
||||
</Tabs>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setOpen(false)}>
|
||||
取消
|
||||
@@ -214,4 +264,3 @@ export function NodeSelect({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import * as LucideIcons from "lucide-react";
|
||||
import { Circle, type LucideIcon } from "lucide-react";
|
||||
|
||||
import type { NodeSpecDto, SystemToolKind, TurnConfig } from "@/lib/api";
|
||||
import type { NodeSpecDto, TurnConfig } from "@/lib/api";
|
||||
import { defaultTurnConfig } from "@/lib/turn-config";
|
||||
|
||||
export type WorkflowNodeType =
|
||||
@@ -41,7 +41,6 @@ export type WorkflowNodeData = {
|
||||
contextPolicy?: ContextPolicy;
|
||||
inheritGlobalConfig?: boolean;
|
||||
entryMode?: AgentEntryMode;
|
||||
systemTools?: SystemToolKind[];
|
||||
stateVariableNames?: string[];
|
||||
toolIds?: string[];
|
||||
knowledgeBaseId?: string;
|
||||
@@ -283,7 +282,6 @@ export function defaultGraph(): WorkflowGraph {
|
||||
contextPolicy: "inherit",
|
||||
inheritGlobalConfig: true,
|
||||
entryMode: "wait_user",
|
||||
systemTools: [],
|
||||
stateVariableNames: [],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import type { KnowledgeRetrievalConfig, TurnConfig } from "@/lib/api";
|
||||
import type {
|
||||
KnowledgeRetrievalConfig,
|
||||
SystemToolKind,
|
||||
Tool,
|
||||
TurnConfig,
|
||||
} from "@/lib/api";
|
||||
|
||||
import type { WorkflowGraph } from "./specs";
|
||||
|
||||
@@ -18,7 +23,13 @@ export type WorkflowSettings = {
|
||||
turnConfig: TurnConfig;
|
||||
};
|
||||
|
||||
export type ModelOption = { value: string; label: string; disabled?: boolean };
|
||||
export type ModelOption = {
|
||||
value: string;
|
||||
label: string;
|
||||
disabled?: boolean;
|
||||
toolType?: Tool["type"];
|
||||
systemKind?: SystemToolKind;
|
||||
};
|
||||
|
||||
export type WorkflowEditorProps = {
|
||||
value?: WorkflowGraph;
|
||||
|
||||
@@ -232,7 +232,6 @@ export type Assistant = {
|
||||
enableInterrupt: boolean;
|
||||
turnConfig: TurnConfig;
|
||||
startup: StartupConfig;
|
||||
systemTools: SystemToolKind[];
|
||||
visionEnabled: boolean;
|
||||
visionModelResourceId: string | null;
|
||||
modelResourceIds: Partial<Record<ModelType, string>>;
|
||||
@@ -365,7 +364,7 @@ export type SystemToolDefinition = {
|
||||
schemaVersion: number;
|
||||
type: "system";
|
||||
config: {
|
||||
kind: "end_conversation";
|
||||
kind: SystemToolKind;
|
||||
messageType: "none" | "custom";
|
||||
customMessage: string;
|
||||
captureReason: boolean;
|
||||
|
||||
Reference in New Issue
Block a user