feat: organize tools and enable qwen realtime calls
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import type { BuiltInTools, DynamicVariableDefinition } from "@/lib/api";
|
||||
|
||||
const DEFAULTS = {
|
||||
endConversation: {
|
||||
description: "礼貌地结束本次对话。",
|
||||
messageType: "none" as const,
|
||||
customMessage: "",
|
||||
captureReason: true,
|
||||
allowEndInterruption: false,
|
||||
},
|
||||
updateState: { description: "更新已声明的会话状态。", updates: [] },
|
||||
skipTurn: { description: "跳过当前轮次,不生成语音回复。" },
|
||||
requestHumanHandoff: { description: "提交人工接管请求。" },
|
||||
};
|
||||
|
||||
function UpdateRulesEditor({
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
value: NonNullable<BuiltInTools["updateState"]>["updates"];
|
||||
onChange: (value: NonNullable<BuiltInTools["updateState"]>["updates"]) => void;
|
||||
}) {
|
||||
const [draft, setDraft] = useState(JSON.stringify(value, null, 2));
|
||||
const [error, setError] = useState("");
|
||||
|
||||
function commit() {
|
||||
try {
|
||||
const parsed = JSON.parse(draft) as unknown;
|
||||
if (!Array.isArray(parsed)) throw new Error();
|
||||
setError("");
|
||||
onChange(parsed);
|
||||
} catch {
|
||||
setError("更新规则必须是 JSON 数组");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-3 space-y-1.5">
|
||||
<div className="text-xs font-medium text-muted-foreground">
|
||||
更新规则(JSON,支持 constant / variable / llm / if / 运算表达式)
|
||||
</div>
|
||||
<Textarea
|
||||
rows={8}
|
||||
className="font-mono text-xs"
|
||||
value={draft}
|
||||
onChange={(event) => setDraft(event.target.value)}
|
||||
onBlur={commit}
|
||||
/>
|
||||
{error && <p className="text-xs text-destructive">{error}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function BuiltInToolsEditor({
|
||||
value,
|
||||
variables,
|
||||
onChange,
|
||||
allowedKeys,
|
||||
}: {
|
||||
value: BuiltInTools;
|
||||
variables: Record<string, DynamicVariableDefinition>;
|
||||
onChange: (value: BuiltInTools) => void;
|
||||
allowedKeys?: Array<keyof BuiltInTools>;
|
||||
}) {
|
||||
const items = [
|
||||
["endConversation", "结束对话"],
|
||||
["updateState", "更新状态"],
|
||||
["skipTurn", "跳过本轮"],
|
||||
["requestHumanHandoff", "转接人工"],
|
||||
] as const;
|
||||
|
||||
function toggle(key: keyof BuiltInTools, enabled: boolean) {
|
||||
if (!enabled) {
|
||||
onChange({ ...value, [key]: null });
|
||||
return;
|
||||
}
|
||||
if (key === "updateState") {
|
||||
const updates = Object.entries(variables).map(([name, definition]) => ({
|
||||
variableName: name,
|
||||
expression: {
|
||||
type: "llm" as const,
|
||||
parameterName: name,
|
||||
valueType: definition.type,
|
||||
description: `从当前对话中提取并确认 ${name}`,
|
||||
},
|
||||
}));
|
||||
onChange({
|
||||
...value,
|
||||
updateState: { ...DEFAULTS.updateState, updates },
|
||||
});
|
||||
return;
|
||||
}
|
||||
onChange({ ...value, [key]: { ...DEFAULTS[key] } });
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{items
|
||||
.filter(([key]) => !allowedKeys || allowedKeys.includes(key))
|
||||
.map(([key, label]) => {
|
||||
const config = value[key];
|
||||
return (
|
||||
<div key={key} className="rounded-xl border border-hairline p-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<div className="text-sm font-medium text-foreground">{label}</div>
|
||||
<div className="text-xs text-muted-foreground">平台内建能力,不创建工具资源。</div>
|
||||
</div>
|
||||
<Switch checked={Boolean(config)} onCheckedChange={(enabled) => toggle(key, enabled)} />
|
||||
</div>
|
||||
{config && (
|
||||
<Input
|
||||
className="mt-3"
|
||||
value={config.description}
|
||||
onChange={(event) =>
|
||||
onChange({ ...value, [key]: { ...config, description: event.target.value } })
|
||||
}
|
||||
placeholder="模型何时调用该能力"
|
||||
/>
|
||||
)}
|
||||
{key === "endConversation" && value.endConversation && (
|
||||
<div className="mt-3 space-y-3 text-sm text-foreground">
|
||||
<label className="flex items-center justify-between gap-3">
|
||||
<span>播报自定义结束语</span>
|
||||
<Switch
|
||||
checked={value.endConversation.messageType === "custom"}
|
||||
onCheckedChange={(enabled) =>
|
||||
onChange({
|
||||
...value,
|
||||
endConversation: {
|
||||
...value.endConversation!,
|
||||
messageType: enabled ? "custom" : "none",
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
{value.endConversation.messageType === "custom" && (
|
||||
<Input
|
||||
value={value.endConversation.customMessage}
|
||||
onChange={(event) =>
|
||||
onChange({
|
||||
...value,
|
||||
endConversation: {
|
||||
...value.endConversation!,
|
||||
customMessage: event.target.value,
|
||||
},
|
||||
})
|
||||
}
|
||||
placeholder="例如:感谢您的时间,再见。"
|
||||
/>
|
||||
)}
|
||||
<label className="flex items-center justify-between gap-3">
|
||||
<span>记录结束原因</span>
|
||||
<Switch
|
||||
checked={value.endConversation.captureReason}
|
||||
onCheckedChange={(captureReason) =>
|
||||
onChange({
|
||||
...value,
|
||||
endConversation: { ...value.endConversation!, captureReason },
|
||||
})
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<label className="flex items-center justify-between gap-3">
|
||||
<span>结束语允许用户打断</span>
|
||||
<Switch
|
||||
checked={value.endConversation.allowEndInterruption}
|
||||
onCheckedChange={(allowEndInterruption) =>
|
||||
onChange({
|
||||
...value,
|
||||
endConversation: {
|
||||
...value.endConversation!,
|
||||
allowEndInterruption,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
{key === "updateState" && value.updateState && (
|
||||
<UpdateRulesEditor
|
||||
value={value.updateState.updates}
|
||||
onChange={(updates) =>
|
||||
onChange({
|
||||
...value,
|
||||
updateState: { ...value.updateState!, updates },
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -18,10 +18,7 @@ import {
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@/components/ui/tabs";
|
||||
import {
|
||||
actionResultAssignmentMode,
|
||||
type WorkflowGraph,
|
||||
} from "@/components/workflow/specs";
|
||||
import type { WorkflowGraph } from "@/components/workflow/specs";
|
||||
import type { DynamicVariableDefinition } from "@/lib/api";
|
||||
|
||||
const DYNAMIC_VARIABLE_PATTERN = /{{\s*([A-Za-z][A-Za-z0-9_]{0,63})\s*}}/g;
|
||||
@@ -51,6 +48,33 @@ function extractDynamicVariableNames(...templates: string[]): string[] {
|
||||
return [...names];
|
||||
}
|
||||
|
||||
function collectStructuredVariableNames(value: unknown, names: Set<string>): void {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((item) => collectStructuredVariableNames(item, names));
|
||||
return;
|
||||
}
|
||||
if (!value || typeof value !== "object") return;
|
||||
const record = value as Record<string, unknown>;
|
||||
if (
|
||||
typeof record.variableName === "string" &&
|
||||
isPublicDynamicVariableName(record.variableName)
|
||||
) {
|
||||
names.add(record.variableName);
|
||||
}
|
||||
if (
|
||||
record.resultAssignments &&
|
||||
!Array.isArray(record.resultAssignments) &&
|
||||
typeof record.resultAssignments === "object"
|
||||
) {
|
||||
Object.keys(record.resultAssignments).forEach((name) => {
|
||||
if (isPublicDynamicVariableName(name)) names.add(name);
|
||||
});
|
||||
}
|
||||
Object.values(record).forEach((item) =>
|
||||
collectStructuredVariableNames(item, names),
|
||||
);
|
||||
}
|
||||
|
||||
function definitionFor(
|
||||
name: string,
|
||||
saved: Record<string, DynamicVariableDefinition>,
|
||||
@@ -67,9 +91,12 @@ function definitionFor(
|
||||
export function activeDynamicVariableDefinitions(
|
||||
templates: string[],
|
||||
saved: Record<string, DynamicVariableDefinition>,
|
||||
structuredValues: unknown[] = [],
|
||||
): Record<string, DynamicVariableDefinition> {
|
||||
const names = new Set(extractDynamicVariableNames(...templates));
|
||||
structuredValues.forEach((value) => collectStructuredVariableNames(value, names));
|
||||
return Object.fromEntries(
|
||||
extractDynamicVariableNames(...templates).map((name) => [
|
||||
[...names].sort().map((name) => [
|
||||
name,
|
||||
definitionFor(name, saved),
|
||||
]),
|
||||
@@ -81,24 +108,13 @@ export function activeWorkflowDynamicVariableDefinitions(
|
||||
saved: Record<string, DynamicVariableDefinition>,
|
||||
): Record<string, DynamicVariableDefinition> {
|
||||
const names = new Set(extractDynamicVariableNames(JSON.stringify(graph)));
|
||||
collectStructuredVariableNames(graph, names);
|
||||
|
||||
for (const edge of graph.edges) {
|
||||
for (const rule of edge.data.expression?.rules ?? []) {
|
||||
if (isPublicDynamicVariableName(rule.variable)) names.add(rule.variable);
|
||||
}
|
||||
}
|
||||
for (const node of graph.nodes) {
|
||||
if (
|
||||
node.type === "action" &&
|
||||
actionResultAssignmentMode(node.data) !== "override"
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
for (const name of Object.keys(node.data.resultAssignments ?? {})) {
|
||||
if (isPublicDynamicVariableName(name)) names.add(name);
|
||||
}
|
||||
}
|
||||
|
||||
return Object.fromEntries(
|
||||
[...names]
|
||||
.sort()
|
||||
|
||||
@@ -8,13 +8,8 @@ import {
|
||||
ChevronLeft,
|
||||
Copy,
|
||||
Pencil,
|
||||
Plus,
|
||||
ServerCog,
|
||||
Settings2,
|
||||
Sparkles,
|
||||
Waypoints,
|
||||
Wrench,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
|
||||
import { HelpHint } from "@/components/editor/section-card";
|
||||
@@ -36,17 +31,8 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@/components/ui/tabs";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import type {
|
||||
KnowledgeRetrievalConfig,
|
||||
Tool,
|
||||
} from "@/lib/api";
|
||||
import type { KnowledgeRetrievalConfig } from "@/lib/api";
|
||||
|
||||
import type { RuntimeMode } from "./types";
|
||||
|
||||
@@ -493,171 +479,6 @@ export function KnowledgeRetrievalConfigDialog({
|
||||
);
|
||||
}
|
||||
|
||||
export function ToolPicker({
|
||||
tools,
|
||||
selectedIds,
|
||||
onChange,
|
||||
}: {
|
||||
tools: Tool[];
|
||||
selectedIds: string[];
|
||||
onChange: (ids: string[]) => void;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<Tool["type"]>("system");
|
||||
const [draftIds, setDraftIds] = useState<string[]>(selectedIds);
|
||||
const selectedTools = selectedIds
|
||||
.map((id) => tools.find((tool) => tool.id === id))
|
||||
.filter((tool): tool is Tool => Boolean(tool));
|
||||
|
||||
function openPicker() {
|
||||
setDraftIds(selectedIds);
|
||||
setOpen(true);
|
||||
}
|
||||
|
||||
function toggleResourceTool(toolId: string) {
|
||||
setDraftIds((current) =>
|
||||
current.includes(toolId)
|
||||
? current.filter((id) => id !== toolId)
|
||||
: [...current, toolId],
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<>
|
||||
<div className="flex min-h-9 flex-wrap items-center gap-2">
|
||||
{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" ? (
|
||||
<Sparkles size={14} />
|
||||
) : tool.type === "mcp" ? (
|
||||
<ServerCog size={14} />
|
||||
) : (
|
||||
<Wrench size={14} />
|
||||
)}
|
||||
<span className="max-w-48 truncate">{tool.name}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onChange(selectedIds.filter((id) => id !== tool.id))}
|
||||
className="text-muted-soft transition-colors hover:text-foreground"
|
||||
aria-label={`移除工具 ${tool.name}`}
|
||||
>
|
||||
<X size={13} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
className="border-hairline-strong text-muted-foreground hover:text-foreground"
|
||||
onClick={openPicker}
|
||||
aria-label="添加工具"
|
||||
title="添加工具"
|
||||
>
|
||||
<Plus size={15} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent className="sm:max-w-xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>选择工具</DialogTitle>
|
||||
<DialogDescription>
|
||||
按类型选择已经启用的工具资源。
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<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 resources = tools.filter((tool) => tool.type === tab.value);
|
||||
const isEmpty = resources.length === 0;
|
||||
|
||||
return (
|
||||
<TabsContent key={tab.value} value={tab.value} className="pt-3">
|
||||
{isEmpty ? (
|
||||
<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">
|
||||
{resources.map((tool) => {
|
||||
const checked = draftIds.includes(tool.id);
|
||||
return (
|
||||
<label
|
||||
key={tool.id}
|
||||
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={() => toggleResourceTool(tool.id)}
|
||||
className="size-4 accent-primary"
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<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}
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
);
|
||||
})}
|
||||
</Tabs>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setOpen(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
onChange(draftIds);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
确认
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function ToggleRow({
|
||||
icon,
|
||||
title,
|
||||
|
||||
@@ -33,12 +33,13 @@ import {
|
||||
ResourceSelectField,
|
||||
RuntimeModeSelector,
|
||||
TextAreaField,
|
||||
ToolPicker,
|
||||
} from "@/components/assistant-editor/editor-controls";
|
||||
import type { AssistantForm } from "@/components/assistant-editor/types";
|
||||
import { SectionAnchorTabs } from "@/components/editor/section-anchor-tabs";
|
||||
import { SectionCard } from "@/components/editor/section-card";
|
||||
import { VisionConfigSection } from "@/components/editor/vision-config-section";
|
||||
import { BuiltInToolsEditor } from "@/components/assistant-editor/built-in-tools";
|
||||
import { ToolBindingEditor } from "@/components/tools/tool-binding-editor";
|
||||
import { TurnConfigEditor } from "@/components/turn-config-editor";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -56,7 +57,13 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import type { DynamicVariableDefinition, StartupConfig, Tool } from "@/lib/api";
|
||||
import type {
|
||||
BuiltInTools,
|
||||
DynamicVariableDefinition,
|
||||
RealtimePromptToolFeatures,
|
||||
StartupConfig,
|
||||
Tool,
|
||||
} from "@/lib/api";
|
||||
|
||||
type ResourceOption = { value: string; label: string };
|
||||
type PromptOpeningMode = StartupConfig["openingMode"];
|
||||
@@ -117,6 +124,7 @@ type PromptEditorProps = {
|
||||
visionModelOptions: ResourceOption[];
|
||||
knowledgeOptions: ResourceOption[];
|
||||
tools: Tool[];
|
||||
realtimeToolFeatures?: RealtimePromptToolFeatures;
|
||||
onBack: () => void;
|
||||
onSave: () => void;
|
||||
onDelete?: () => void | Promise<void>;
|
||||
@@ -142,6 +150,7 @@ export function PromptEditor({
|
||||
visionModelOptions,
|
||||
knowledgeOptions: kbOptions,
|
||||
tools,
|
||||
realtimeToolFeatures,
|
||||
onBack,
|
||||
onSave,
|
||||
onDelete,
|
||||
@@ -587,16 +596,6 @@ export function PromptEditor({
|
||||
openingMessage: null,
|
||||
});
|
||||
}
|
||||
if (runtimeMode === "realtime") {
|
||||
updateForm(
|
||||
"toolIds",
|
||||
form.toolIds.filter(
|
||||
(id) =>
|
||||
tools.find((tool) => tool.id === id)?.type !==
|
||||
"system",
|
||||
),
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -692,20 +691,121 @@ export function PromptEditor({
|
||||
</SectionCard>
|
||||
)}
|
||||
|
||||
{(form.runtimeMode === "pipeline" ||
|
||||
Boolean(realtimeToolFeatures)) && (
|
||||
<SectionCard
|
||||
icon={<Sparkles size={15} />}
|
||||
title="内建能力"
|
||||
description="配置平台提供的结束、状态更新与人工接管能力"
|
||||
>
|
||||
<BuiltInToolsEditor
|
||||
value={form.builtInTools}
|
||||
variables={dynamicVariableDefinitions}
|
||||
allowedKeys={
|
||||
form.runtimeMode === "realtime"
|
||||
? realtimeToolFeatures?.builtIns
|
||||
: undefined
|
||||
}
|
||||
onChange={(builtInTools) =>
|
||||
updateForm("builtInTools", builtInTools)
|
||||
}
|
||||
/>
|
||||
</SectionCard>
|
||||
)}
|
||||
|
||||
{form.runtimeMode === "realtime" && !realtimeToolFeatures && (
|
||||
<SectionCard
|
||||
icon={<Sparkles size={15} />}
|
||||
title="Realtime 工具"
|
||||
description="当前 Realtime 接口尚未开放 Prompt 工具调用"
|
||||
>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
已有配置会被保留,但保存前需要切换到支持工具的模型或清理这些配置。
|
||||
</p>
|
||||
</SectionCard>
|
||||
)}
|
||||
|
||||
{form.runtimeMode === "realtime" && (() => {
|
||||
const allowedBuiltIns = new Set(
|
||||
realtimeToolFeatures?.builtIns ?? [],
|
||||
);
|
||||
const unsupportedBuiltIns = (
|
||||
Object.keys(form.builtInTools) as Array<keyof BuiltInTools>
|
||||
).filter(
|
||||
(key) =>
|
||||
Boolean(form.builtInTools[key]) &&
|
||||
!allowedBuiltIns.has(key),
|
||||
);
|
||||
const allowedExternal = new Set(
|
||||
realtimeToolFeatures?.externalTypes ?? [],
|
||||
);
|
||||
const unsupportedToolIds = new Set(
|
||||
form.toolBindings
|
||||
.filter((binding) => {
|
||||
const tool = tools.find((item) => item.id === binding.toolId);
|
||||
return !tool || !allowedExternal.has(tool.type);
|
||||
})
|
||||
.map((binding) => binding.toolId),
|
||||
);
|
||||
if (!unsupportedBuiltIns.length && !unsupportedToolIds.size) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<SectionCard
|
||||
icon={<Wrench size={15} />}
|
||||
title="存在不兼容工具配置"
|
||||
description="这些配置不会被自动删除,后端也会阻止保存"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{unsupportedBuiltIns.length} 个内建工具,
|
||||
{unsupportedToolIds.size} 个外部工具不受当前接口支持。
|
||||
</p>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
updateForm(
|
||||
"builtInTools",
|
||||
Object.fromEntries(
|
||||
Object.entries(form.builtInTools).filter(([key]) =>
|
||||
allowedBuiltIns.has(key as keyof BuiltInTools),
|
||||
),
|
||||
) as BuiltInTools,
|
||||
);
|
||||
updateForm(
|
||||
"toolBindings",
|
||||
form.toolBindings.filter(
|
||||
(binding) => !unsupportedToolIds.has(binding.toolId),
|
||||
),
|
||||
);
|
||||
}}
|
||||
>
|
||||
清理不兼容配置
|
||||
</Button>
|
||||
</div>
|
||||
</SectionCard>
|
||||
);
|
||||
})()}
|
||||
|
||||
<SectionCard
|
||||
icon={<Wrench size={15} />}
|
||||
title="工具"
|
||||
description="配置该提示词助手可以调用的工具"
|
||||
title="外部工具"
|
||||
description="选择可复用资源,并配置当前助手的参数来源与结果映射"
|
||||
>
|
||||
<ToolPicker
|
||||
tools={tools.filter(
|
||||
(tool) =>
|
||||
tool.status === "active" &&
|
||||
(form.runtimeMode === "pipeline" ||
|
||||
tool.type !== "system"),
|
||||
)}
|
||||
selectedIds={form.toolIds}
|
||||
onChange={(toolIds) => updateForm("toolIds", toolIds)}
|
||||
<ToolBindingEditor
|
||||
tools={
|
||||
form.runtimeMode === "realtime"
|
||||
? tools.filter((tool) =>
|
||||
realtimeToolFeatures?.externalTypes.includes(tool.type),
|
||||
)
|
||||
: tools
|
||||
}
|
||||
value={form.toolBindings}
|
||||
variables={dynamicVariableDefinitions}
|
||||
onChange={(toolBindings) =>
|
||||
updateForm("toolBindings", toolBindings)
|
||||
}
|
||||
/>
|
||||
</SectionCard>
|
||||
</section>
|
||||
|
||||
@@ -3,6 +3,8 @@ import type {
|
||||
DynamicVariableDefinition,
|
||||
KnowledgeRetrievalConfig,
|
||||
StartupConfig,
|
||||
BuiltInTools,
|
||||
ToolBinding,
|
||||
TurnConfig,
|
||||
VisionPromptMode,
|
||||
} from "@/lib/api";
|
||||
@@ -29,5 +31,6 @@ export type AssistantForm = {
|
||||
visionModelResourceId: string;
|
||||
visionPromptMode: VisionPromptMode;
|
||||
visionPrompt: string;
|
||||
toolIds: string[];
|
||||
toolBindings: ToolBinding[];
|
||||
builtInTools: BuiltInTools;
|
||||
};
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { TopbarPortal } from "@/components/layout/topbar-portal";
|
||||
import { WorkflowEditor } from "@/components/workflow/WorkflowEditor";
|
||||
import type { WorkflowSettings } from "@/components/workflow/types";
|
||||
import type { ModelOption, WorkflowSettings } from "@/components/workflow/types";
|
||||
import {
|
||||
workflowUsesVision,
|
||||
type WorkflowGraph,
|
||||
@@ -43,7 +43,7 @@ type WorkflowPageProps = {
|
||||
tts: ResourceOption[];
|
||||
vision: ResourceOption[];
|
||||
};
|
||||
toolOptions: ResourceOption[];
|
||||
toolOptions: ModelOption[];
|
||||
knowledgeOptions: ResourceOption[];
|
||||
onBack: () => void;
|
||||
onSave: () => void;
|
||||
@@ -195,6 +195,7 @@ export function WorkflowPage({
|
||||
value,
|
||||
label: `${value} · ${definition.type}`,
|
||||
}))}
|
||||
dynamicVariableDefinitions={dynamicVariableDefinitions}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user