Add Dify integration and enhance workflow node specifications
- Introduce new fields `dify_api_url` and `dify_api_key` in `AssistantConfig` for Dify API integration. - Update `requirements.txt` to include `dify-client-python` for Dify SDK support. - Modify `config_resolver` to handle Dify connection information. - Add a new `globalNode` type in workflow specifications to provide unified settings across workflows. - Enhance node specifications with additional constraints and default values for better configuration management. - Update frontend components to support the new `globalNode` type and its properties, improving workflow editor functionality.
This commit is contained in:
@@ -144,4 +144,5 @@ export const nodeTypes = {
|
||||
startCall: GenericNode,
|
||||
agentNode: GenericNode,
|
||||
endCall: GenericNode,
|
||||
globalNode: GenericNode,
|
||||
};
|
||||
|
||||
@@ -90,6 +90,14 @@ export type WorkflowEditorProps = {
|
||||
let nodeSeq = 0;
|
||||
const NONE = "__none__";
|
||||
|
||||
function defaultNodeData(spec: RuntimeNodeSpec): WorkflowNodeData {
|
||||
const data: WorkflowNodeData = { name: spec.displayName };
|
||||
for (const field of spec.fields) {
|
||||
if (field.default !== undefined) data[field.key] = field.default;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
function toFlow(graph: WorkflowGraph): { nodes: Node[]; edges: Edge[] } {
|
||||
return {
|
||||
nodes: graph.nodes.map((n) => ({
|
||||
@@ -179,12 +187,34 @@ function Canvas({
|
||||
const isValidConnection = useCallback(
|
||||
(c: Connection | Edge) => {
|
||||
if (c.source === c.target) return false;
|
||||
const source = nodes.find((n) => n.id === c.source);
|
||||
const target = nodes.find((n) => n.id === c.target);
|
||||
if (!target) return false;
|
||||
const spec = specsByType[target.type as string];
|
||||
return !!spec?.hasTarget;
|
||||
if (!source || !target) return false;
|
||||
|
||||
const sourceSpec = specsByType[source.type as string];
|
||||
const targetSpec = specsByType[target.type as string];
|
||||
if (!sourceSpec?.hasSource || !targetSpec?.hasTarget) return false;
|
||||
if (edges.some((e) => e.source === c.source && e.target === c.target)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const sourceLimit = sourceSpec.constraints.maxOutgoing;
|
||||
if (
|
||||
sourceLimit !== undefined &&
|
||||
edges.filter((e) => e.source === c.source).length >= sourceLimit
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const targetLimit = targetSpec.constraints.maxIncoming;
|
||||
if (
|
||||
targetLimit !== undefined &&
|
||||
edges.filter((e) => e.target === c.target).length >= targetLimit
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
[nodes, specsByType],
|
||||
[edges, nodes, specsByType],
|
||||
);
|
||||
|
||||
const addNode = useCallback(
|
||||
@@ -195,11 +225,7 @@ function Canvas({
|
||||
x: window.innerWidth / 2,
|
||||
y: window.innerHeight / 2,
|
||||
});
|
||||
const data: WorkflowNodeData = { name: spec.displayName };
|
||||
if (spec.type === "agentNode") {
|
||||
data.allowInterrupt = true;
|
||||
data.prompt = "";
|
||||
}
|
||||
const data = defaultNodeData(spec);
|
||||
setNodes((ns) => [...ns, { id, type: spec.type, position, data }]);
|
||||
setAddOpen(false);
|
||||
setEditingId(id);
|
||||
@@ -281,6 +307,14 @@ function Canvas({
|
||||
const editingSpec = editingNode ? specsByType[editingNode.type as string] : null;
|
||||
const editingEdge = edges.find((e) => e.id === editingEdgeId);
|
||||
const addableSpecs = Object.values(specsByType).filter((s) => s.addable);
|
||||
const canAddSpec = useCallback(
|
||||
(spec: RuntimeNodeSpec) => {
|
||||
const limit = spec.constraints.maxInstances;
|
||||
if (limit === undefined) return true;
|
||||
return nodes.filter((node) => node.type === spec.type).length < limit;
|
||||
},
|
||||
[nodes],
|
||||
);
|
||||
|
||||
return (
|
||||
<NodeSpecsContext.Provider value={specsByType}>
|
||||
@@ -398,11 +432,13 @@ function Canvas({
|
||||
) : null}
|
||||
{addableSpecs.map((spec) => {
|
||||
const Icon = spec.icon;
|
||||
const canAdd = canAddSpec(spec);
|
||||
return (
|
||||
<button
|
||||
key={spec.type}
|
||||
type="button"
|
||||
className="group relative flex items-start gap-4 overflow-hidden rounded-2xl border border-hairline bg-card p-4 text-left shadow-sm transition-[border-color,box-shadow,transform] hover:-translate-y-0.5 hover:border-hairline-strong hover:shadow-md"
|
||||
disabled={!canAdd}
|
||||
className="group relative flex items-start gap-4 overflow-hidden rounded-2xl border border-hairline bg-card p-4 text-left shadow-sm transition-[border-color,box-shadow,transform] hover:-translate-y-0.5 hover:border-hairline-strong hover:shadow-md disabled:cursor-not-allowed disabled:opacity-55 disabled:hover:translate-y-0 disabled:hover:border-hairline disabled:hover:shadow-sm"
|
||||
onClick={() => addNode(spec)}
|
||||
>
|
||||
<span
|
||||
@@ -421,8 +457,13 @@ function Canvas({
|
||||
<Icon size={17} />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-sm font-medium text-foreground">
|
||||
{spec.displayName}
|
||||
<div className="flex items-center gap-2 text-sm font-medium text-foreground">
|
||||
<span>{spec.displayName}</span>
|
||||
{!canAdd && (
|
||||
<span className="rounded-full bg-surface-strong px-2 py-0.5 text-[10px] font-normal text-muted-foreground">
|
||||
已添加
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-1 text-xs leading-5 text-muted-foreground">
|
||||
{spec.description}
|
||||
@@ -610,6 +651,11 @@ function NodeForm({
|
||||
const [draft, setDraft] = useState<WorkflowNodeData>({ ...data });
|
||||
const set = (key: string, val: unknown) =>
|
||||
setDraft((d) => ({ ...d, [key]: val }));
|
||||
const missingRequired = spec.fields.some((field) => {
|
||||
if (!field.required) return false;
|
||||
const value = draft[field.key];
|
||||
return typeof value === "string" ? !value.trim() : value == null;
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -677,7 +723,9 @@ function NodeForm({
|
||||
) : (
|
||||
<span />
|
||||
)}
|
||||
<Button onClick={() => onSave(draft)}>保存</Button>
|
||||
<Button disabled={missingRequired} onClick={() => onSave(draft)}>
|
||||
保存
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -11,7 +11,11 @@ import { Circle, type LucideIcon } from "lucide-react";
|
||||
|
||||
import type { NodeSpecDto } from "@/lib/api";
|
||||
|
||||
export type WorkflowNodeType = "startCall" | "agentNode" | "endCall";
|
||||
export type WorkflowNodeType =
|
||||
| "startCall"
|
||||
| "agentNode"
|
||||
| "endCall"
|
||||
| "globalNode";
|
||||
|
||||
export type WorkflowNodeData = {
|
||||
/** 节点显示名 */
|
||||
@@ -22,6 +26,8 @@ export type WorkflowNodeData = {
|
||||
prompt?: string;
|
||||
/** 允许打断(仅 agentNode) */
|
||||
allowInterrupt?: boolean;
|
||||
/** 是否合并全局节点提示词(start/agent 默认开启,end 默认关闭) */
|
||||
addGlobalPrompt?: boolean;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
@@ -30,6 +36,7 @@ export type FieldSpec = {
|
||||
label: string;
|
||||
type: "text" | "textarea" | "switch";
|
||||
required?: boolean;
|
||||
default?: unknown;
|
||||
};
|
||||
|
||||
/** 解析后的运行期节点规格(DTO + 解析出的 React 图标 + 派生句柄) */
|
||||
@@ -44,6 +51,14 @@ export type RuntimeNodeSpec = {
|
||||
hasTarget: boolean;
|
||||
/** 出边句柄(结束节点没有) */
|
||||
hasSource: boolean;
|
||||
constraints: {
|
||||
minIncoming?: number;
|
||||
maxIncoming?: number;
|
||||
minOutgoing?: number;
|
||||
maxOutgoing?: number;
|
||||
minInstances?: number;
|
||||
maxInstances?: number;
|
||||
};
|
||||
fields: FieldSpec[];
|
||||
};
|
||||
|
||||
@@ -77,11 +92,13 @@ export function toRuntimeSpec(dto: NodeSpecDto): RuntimeNodeSpec {
|
||||
addable: dto.addable,
|
||||
hasTarget: dto.constraints.maxIncoming !== 0,
|
||||
hasSource: dto.constraints.maxOutgoing !== 0,
|
||||
constraints: dto.constraints,
|
||||
fields: dto.fields.map((f) => ({
|
||||
key: f.key,
|
||||
label: f.label,
|
||||
type: f.type,
|
||||
required: f.required,
|
||||
default: f.default,
|
||||
})),
|
||||
};
|
||||
}
|
||||
@@ -104,27 +121,52 @@ export type WorkflowGraph = {
|
||||
viewport?: { x: number; y: number; zoom: number };
|
||||
};
|
||||
|
||||
/** 新建工作流的默认图:开始 → 智能体 → 结束 */
|
||||
/** 新建工作流的默认图:全局规则 + 开始 → 智能体 → 结束 */
|
||||
export function defaultGraph(): WorkflowGraph {
|
||||
return {
|
||||
nodes: [
|
||||
{
|
||||
id: "start",
|
||||
type: "startCall",
|
||||
position: { x: 80, y: 160 },
|
||||
data: { name: "开始", greeting: "你好,我是 AI 视频助手,有什么可以帮你?" },
|
||||
position: { x: 100, y: 120 },
|
||||
data: {
|
||||
name: "开始",
|
||||
greeting: "你好,我是 AI 视频助手,有什么可以帮你?",
|
||||
prompt: "了解用户的需求,并在信息明确后进入下一节点。",
|
||||
allowInterrupt: true,
|
||||
addGlobalPrompt: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "agent-1",
|
||||
type: "agentNode",
|
||||
position: { x: 400, y: 160 },
|
||||
data: { name: "智能体节点", prompt: "", allowInterrupt: true },
|
||||
position: { x: 420, y: 120 },
|
||||
data: {
|
||||
name: "智能体节点",
|
||||
prompt: "根据用户需求提供清晰、准确的帮助。",
|
||||
allowInterrupt: true,
|
||||
addGlobalPrompt: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "end",
|
||||
type: "endCall",
|
||||
position: { x: 720, y: 160 },
|
||||
data: { name: "结束", prompt: "感谢你的来电,再见!" },
|
||||
position: { x: 740, y: 120 },
|
||||
data: {
|
||||
name: "结束",
|
||||
prompt: "总结已经完成的事项,礼貌道别并结束通话。",
|
||||
addGlobalPrompt: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "global",
|
||||
type: "globalNode",
|
||||
position: { x: 100, y: 400 },
|
||||
data: {
|
||||
name: "全局设定",
|
||||
prompt:
|
||||
"你是一个友好、专业的语音助手。请使用简短、自然、适合口语表达的句子。",
|
||||
},
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
|
||||
Reference in New Issue
Block a user