refactor: unify system tools as resources

This commit is contained in:
Xin Wang
2026-08-04 17:05:26 +08:00
parent d1b05f16c7
commit 74a8be2357
26 changed files with 788 additions and 507 deletions
@@ -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>
);
}
+1 -3
View File
@@ -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: [],
},
},
+13 -2
View File
@@ -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;