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:
Xin Wang
2026-07-11 22:26:31 +08:00
parent dfb9c5bd11
commit 00270a5c01
23 changed files with 1270 additions and 414 deletions

View File

@@ -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>
</>
);