From 0f86905d6b40b290e0d72682a1efd84ae0e29c46 Mon Sep 17 00:00:00 2001 From: 2233admin <2233admin@users.noreply.github.com> Date: Tue, 28 Jul 2026 02:21:43 +0800 Subject: [PATCH 1/2] feat(studio): complete workflow authoring and OODA sources --- frontend/components/flow/command-palette.tsx | 30 ++- frontend/components/flow/inspector-shell.tsx | 10 +- frontend/components/flow/inspector.tsx | 206 +++++++++++++++--- .../components/flow/nodes/workflow-node.tsx | 49 +++-- frontend/lib/workflow/node-primitives.ts | 96 ++++++++ .../workflow/opencli-business-workflows.ts | 127 ++++++++++- frontend/lib/workflow/studio-templates.ts | 2 +- frontend/lib/workflow/workflow-outline.ts | 90 +++++++- frontend/package.json | 2 +- .../scripts/check-dify-p0-regressions.mjs | 51 ++++- .../check-inspector-workflow-regressions.mjs | 150 +++++++++++++ .../check-opencli-business-workflows.mjs | 18 +- .../scripts/check-workflow-regressions.mjs | 10 +- 13 files changed, 769 insertions(+), 72 deletions(-) create mode 100644 frontend/scripts/check-inspector-workflow-regressions.mjs diff --git a/frontend/components/flow/command-palette.tsx b/frontend/components/flow/command-palette.tsx index 820310f..79fab15 100644 --- a/frontend/components/flow/command-palette.tsx +++ b/frontend/components/flow/command-palette.tsx @@ -46,7 +46,11 @@ import { workflowNodeDepthFromNetworkStack, workflowNodeLayerAtDepth } from "@/l import { localizeNodeText, type WorkflowLanguage } from "@/lib/workflow/node-i18n" import { groupPrimitivesForNodeMenu } from "@/lib/workflow/node-menu" import { getNodeContractByCatalogId } from "@/lib/workflow/node-contracts" -import { getWorkflowPrimitives, type WorkflowPrimitive } from "@/lib/workflow/node-primitives" +import { + getDifyCommonWorkflowPrimitives, + getWorkflowPrimitives, + type WorkflowPrimitive, +} from "@/lib/workflow/node-primitives" import { openCLIAdapterNodeToCatalogItem } from "@/lib/workflow/opencli-adapter-catalog" import { useWorkflowCapabilities } from "@/lib/workflow/use-workflow-capabilities" import { cn } from "@/lib/utils" @@ -642,6 +646,7 @@ export function CommandPalette({ workflowCatalogPluginProvenance(item) === null && item.runtimeCapability?.source !== "backend.workflow.tool_capabilities", ) + const catalogOperatorIds = new Set(catalogOperators.map((item) => item.id)) const pluginTools = allCatalogItems.filter( (item) => catalogAcceptsConnection(item, compatiblePort) && @@ -679,7 +684,8 @@ export function CommandPalette({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [capabilities, catalogItems, compatiblePort, inNodeNetwork, language, queryText, workflowProfile]) const primitiveGroups = groupPrimitivesForNodeMenu( - (inNodeNetwork ? getWorkflowPrimitives() : []).filter((item) => { + (inNodeNetwork ? getWorkflowPrimitives() : getDifyCommonWorkflowPrimitives()).filter((item) => { + if (catalogOperatorIds.has(item.id)) return false if (!primitiveAcceptsConnection(item, compatiblePort)) return false if (!queryText) return true const text = localizeNodeText(item.id, { label: item.label, description: item.description }, language) @@ -748,9 +754,23 @@ export function CommandPalette({
{selectedPresentation.label}
{copy.requiredBeforeAdd}
- {selectedOpenCLI.args.filter((arg) => arg.required).map((arg) => ( - - ))} + {selectedOpenCLI.args.filter((arg) => arg.required).map((arg) => { + const value = requiredValues[arg.name] ?? (arg.default == null ? "" : String(arg.default)) + const onChange = (next: string) => setRequiredValues((current) => ({ ...current, [arg.name]: next })) + return ( + + ) + })}
diff --git a/frontend/components/flow/inspector-shell.tsx b/frontend/components/flow/inspector-shell.tsx index a7079bf..f7f3d9d 100644 --- a/frontend/components/flow/inspector-shell.tsx +++ b/frontend/components/flow/inspector-shell.tsx @@ -4,7 +4,7 @@ import { useRef, useState, type KeyboardEvent, type PointerEvent, type ReactNode import { LocateFixed, Pin, X } from "lucide-react" import { cn } from "@/lib/utils" -const stateText: Record = { +export const workflowStatusText: Record = { idle: "Idle", running: "Running", success: "Done", @@ -12,7 +12,7 @@ const stateText: Record = { error: "Error", } -const stateDotClass: Record = { +export const workflowStatusDotClass: Record = { idle: "border-muted-foreground/50 bg-transparent", running: "border-info bg-info", success: "border-success bg-success", @@ -54,10 +54,10 @@ function PanelStatus({ status }: { status?: string }) { return ( - - {stateText[status] ?? status} + + {workflowStatusText[status] ?? status} ) } diff --git a/frontend/components/flow/inspector.tsx b/frontend/components/flow/inspector.tsx index 486c70f..5b73202 100644 --- a/frontend/components/flow/inspector.tsx +++ b/frontend/components/flow/inspector.tsx @@ -4,11 +4,13 @@ import { useEffect, useState } from "react" import Link from "next/link" import { AlertTriangle, + ChevronRight, Database, ExternalLink, GitBranch, Plus, PlugZap, + Search, Trash2, Unplug, } from "lucide-react" @@ -53,7 +55,14 @@ import { shouldPreserveNodeAuthoredText, type WorkflowLanguage, } from "@/lib/workflow/node-i18n" -import { buildWorkflowOutlineRows } from "@/lib/workflow/workflow-outline" +import { + buildWorkflowOutlineRows, + filterWorkflowOutlineRows, + visibleWorkflowOutlineRows, + workflowDirectUpstreamNodeIds, + workflowInputReferenceForPort, + workflowOutlineRowHasChildren, +} from "@/lib/workflow/workflow-outline" import { findWorkflowProjectNodeByCanvasId } from "@/lib/workflow/node-path" import { isOpenCLISourceSlotArray, @@ -76,7 +85,13 @@ import type { WorkflowProject, WorkflowProjectNode, } from "@/lib/workflow/schema" -import { MonoRow, PanelShell, SectionCaption } from "./inspector-shell" +import { + MonoRow, + PanelShell, + SectionCaption, + workflowStatusDotClass, + workflowStatusText, +} from "./inspector-shell" import { cn } from "@/lib/utils" const edgeTypeOptions = [ @@ -101,6 +116,9 @@ const INSPECTOR_COPY = { noNodes: "当前工作流还没有节点。", nodes: "节点", connections: "连线", + outlineSearch: "搜索节点、类型或说明", + collapseNode: "折叠节点", + expandNode: "展开节点", promptSection: "节点提示词配置", promptHelp: "这里只显示节点已保存的提示词配置和测试用例。AI 编辑生成的是待审阅提案,确认应用后才会更新工作流。", configuredPrompt: "已配置提示词", @@ -130,6 +148,7 @@ const INSPECTOR_COPY = { inputsHelp: "每个输入端口只列出类型兼容的真实上游输出。可在这里重接或解绑。", inputUnbound: "未连接", noCompatibleOutputs: "没有可用的兼容输出。", + insertVariable: "引用上游输出", fieldMapping: "字段映射", fieldMappingGap: "上下游契约尚未提供字段 schema,暂不允许手填来源或目标字段路径。已有旧映射只读保留。", legacyMapping: "旧映射", @@ -175,6 +194,9 @@ const INSPECTOR_COPY = { noNodes: "This workflow has no nodes yet.", nodes: "nodes", connections: "connections", + outlineSearch: "Search nodes, types, or descriptions", + collapseNode: "Collapse node", + expandNode: "Expand node", promptSection: "Node prompt configuration", promptHelp: "This shows only saved prompt configuration and test cases. AI edits remain review proposals until you apply them.", configuredPrompt: "Configured prompt", @@ -204,6 +226,7 @@ const INSPECTOR_COPY = { inputsHelp: "Each input lists only type-compatible outputs from real upstream nodes. Reconnect or unbind here.", inputUnbound: "Unbound", noCompatibleOutputs: "No compatible outputs are available.", + insertVariable: "Reference upstream output", fieldMapping: "Field mapping", fieldMappingGap: "The upstream and downstream contracts do not expose field schemas yet. Manual source and target field paths are disabled; legacy mappings remain read-only.", legacyMapping: "Legacy mapping", @@ -378,10 +401,34 @@ function WorkflowOutlinePanel({ workflowProject: WorkflowProject }) { const copy = INSPECTOR_COPY[language] + const [outlineQuery, setOutlineQuery] = useState("") + const [collapsedNodeIds, setCollapsedNodeIds] = useState>(() => new Set()) const nodeById = new Map(nodes.map((node) => [node.id, node])) const rows = buildWorkflowOutlineRows(nodes, edges) - const connectedRows = rows.filter((row) => !row.disconnected) - const disconnectedRows = rows.filter((row) => row.disconnected) + const outlineRowIndexById = new Map(rows.map((row, index) => [row.nodeId, index])) + const filteredRows = filterWorkflowOutlineRows(rows, outlineQuery, (nodeId) => { + const node = nodeById.get(nodeId) + if (!node) return "" + const localized = localizeNodeText( + getNodeDisplayId(node.data), + { label: node.data.label, description: node.data.description }, + language, + ) + return `${node.data.label} ${node.data.description ?? ""} ${localized.label} ${localized.description ?? ""} ${node.data.nodeType}` + }) + const visibleRows = outlineQuery.trim() + ? filteredRows + : visibleWorkflowOutlineRows(filteredRows, collapsedNodeIds) + const connectedRows = visibleRows.filter((row) => !row.disconnected) + const disconnectedRows = visibleRows.filter((row) => row.disconnected) + const toggleOutlineNode = (nodeId: string) => { + setCollapsedNodeIds((current) => { + const next = new Set(current) + if (next.has(nodeId)) next.delete(nodeId) + else next.add(nodeId) + return next + }) + } const renderRows = (sectionRows: typeof rows) => (
{sectionRows.map((row) => { @@ -409,39 +456,64 @@ function WorkflowOutlinePanel({ language, }) : localized.label + const collapsed = collapsedNodeIds.has(node.id) + const status = node.data.status ?? "idle" + const rowIndex = outlineRowIndexById.get(node.id) + const hasChildren = rowIndex !== undefined && workflowOutlineRowHasChildren(rows, rowIndex) return ( - + ) : ( + + )} + + +
) })} @@ -463,6 +535,17 @@ function WorkflowOutlinePanel({ {nodes.length} {copy.nodes} · {edges.length} {copy.connections}

+
+ + setOutlineQuery(event.target.value)} + placeholder={copy.outlineSearch} + aria-label={copy.outlineSearch} + className={cn(houdiniInputClass, "w-full pl-8")} + /> +
{nodes.length === 0 ? (

{copy.noNodes} @@ -930,6 +1013,24 @@ export function Inspector({ compact = false, onClose }: { compact?: boolean; onC

{control}
) + const variableSelector = !field.readonly && upstreamVariableOptions.length > 0 ? ( + + ) : null if (field.type === "json") { const draftKey = `${configurationNodeId}:${field.id}` @@ -1019,6 +1120,8 @@ export function Inspector({ compact = false, onClose }: { compact?: boolean; onC if (field.type === "textarea") { return row( +
+ {variableSelector}