feat: enhance condition handling with enum value options and metadata in workflow components

This commit is contained in:
mlogclub
2026-06-27 12:50:29 +08:00
parent d4ff0980c4
commit f85fb6d406
12 changed files with 497 additions and 41 deletions
@@ -292,6 +292,8 @@ function ConditionNodePanel({
{branches.map((branch, index) => {
const summary = summariesByBranchID.get(branch.id)
const condition = branch.condition ?? {}
const selectedVariable = findConditionVariable(availableVariables, condition.left)
const operatorOptions = getConditionOperatorOptions(selectedVariable)
const conditionRight = condition.right === undefined || condition.right === null
? ""
: String(condition.right)
@@ -344,7 +346,7 @@ function ConditionNodePanel({
<Label className="text-xs"></Label>
<OptionCombobox
value={condition.operator ?? "eq"}
options={conditionOperators}
options={operatorOptions}
placeholder="选择判断方式"
searchPlaceholder="搜索判断方式"
emptyText="没有可用判断方式"
@@ -354,19 +356,13 @@ function ConditionNodePanel({
/>
</div>
{!conditionOperatorWithoutRight(condition.operator ?? "eq") ? (
<div className="space-y-1.5">
<Label className="text-xs"></Label>
<Input
value={conditionRight}
onChange={(event) => commitBranch(branch.id, {
condition: {
...condition,
right: normalizeConditionRight(event.target.value),
},
})}
placeholder="请输入比较值"
/>
</div>
<ConditionRightControl
value={conditionRight}
variable={selectedVariable}
onChange={(right) => commitBranch(branch.id, {
condition: { ...condition, right },
})}
/>
) : null}
</div>
)}
@@ -422,25 +418,126 @@ const conditionOperators = [
{ value: "exists", label: "存在" },
{ value: "not_exists", label: "不存在" },
{ value: "truthy", label: "为真" },
{ value: "is_true", label: "为真" },
{ value: "falsy", label: "为假" },
{ value: "is_false", label: "为假" },
{ value: "gt", label: "大于" },
{ value: "gte", label: "大于等于" },
{ value: "lt", label: "小于" },
{ value: "lte", label: "小于等于" },
]
function conditionOperatorWithoutRight(operator: string) {
return ["exists", "not_exists", "truthy", "falsy"].includes(operator)
function ConditionRightControl({
value,
variable,
onChange,
}: {
value: string
variable?: WorkflowVariableRef
onChange: (value: unknown) => void
}) {
const valueOptions = getConditionValueOptions(variable)
if (valueOptions.length > 0) {
return (
<div className="space-y-1.5">
<Label className="text-xs"></Label>
<OptionCombobox
value={value}
options={valueOptions}
placeholder="选择比较值"
searchPlaceholder="搜索比较值"
emptyText="当前变量没有可选值"
onChange={(nextValue) => onChange(decodeConditionRight(nextValue, variable))}
/>
</div>
)
}
return (
<div className="space-y-1.5">
<Label className="text-xs"></Label>
<Input
type={variable?.type === "number" || variable?.type === "integer" ? "number" : "text"}
value={value}
onChange={(event) => onChange(normalizeConditionRight(event.target.value, variable))}
placeholder={variable ? `请输入${variable.label || variable.field}的比较值` : "请输入比较值"}
/>
</div>
)
}
function normalizeConditionRight(value: string) {
function conditionOperatorWithoutRight(operator: string) {
return ["exists", "not_exists", "truthy", "is_true", "falsy", "is_false"].includes(operator)
}
function normalizeConditionRight(value: string, variable?: WorkflowVariableRef) {
const trimmed = value.trim()
if (variable?.type === "boolean") {
return trimmed === "true"
}
if (variable?.type === "number" || variable?.type === "integer") {
return trimmed === "" ? "" : Number(trimmed)
}
if (variable?.type === "string") {
return trimmed
}
if (trimmed === "true") return true
if (trimmed === "false") return false
if (trimmed !== "" && !Number.isNaN(Number(trimmed))) return Number(trimmed)
return trimmed
}
function findConditionVariable(
variables: WorkflowVariableRef[],
selector?: WorkflowVariableSelector
): WorkflowVariableRef | undefined {
if (!selector?.nodeId || !selector.field) {
return undefined
}
return variables.find((item) => item.nodeId === selector.nodeId && item.field === selector.field)
}
function getConditionOperatorOptions(variable?: WorkflowVariableRef) {
if (!variable?.operators?.length) {
return conditionOperators
}
const allowed = new Set(variable.operators)
return conditionOperators.filter((item) => allowed.has(item.value))
}
function getConditionValueOptions(variable?: WorkflowVariableRef) {
if (variable?.valueOptions?.length) {
return variable.valueOptions.map((item) => ({
value: encodeConditionRight(item.value),
label: item.label,
}))
}
if (variable?.type === "boolean") {
return [
{ value: "true", label: "是" },
{ value: "false", label: "否" },
]
}
return []
}
function encodeConditionRight(value: unknown) {
if (typeof value === "string") return value
if (typeof value === "number" || typeof value === "boolean") return String(value)
return JSON.stringify(value)
}
function decodeConditionRight(value: string, variable?: WorkflowVariableRef) {
if (variable?.type === "boolean") {
return value === "true"
}
if (variable?.type === "number" || variable?.type === "integer") {
return Number(value)
}
const option = variable?.valueOptions?.find((item) => encodeConditionRight(item.value) === value)
return option ? option.value : value
}
function normalizeBranch(branch: WorkflowConditionBranch): WorkflowConditionBranch {
if (branch.default) {
const { condition: _condition, ...rest } = branch
@@ -15,7 +15,7 @@ export function VariableSelector({
}) {
const options = variables.map((item) => ({
value: `${item.nodeId}.${item.field}`,
label: `${item.nodeName}.${item.field} · ${item.type}`,
label: `${item.nodeName}.${item.label || item.field} · ${item.type}`,
}))
const selectedValue = value?.nodeId && value.field ? `${value.nodeId}.${value.field}` : ""
@@ -70,6 +70,7 @@ import {
type WorkflowHistory,
type WorkflowHelperLine,
type WorkflowNodeConfig,
type WorkflowVariableSpec,
} from "./workflow-utils"
import { NodeConfigPanel, type WorkflowBranchSummary } from "./node-config-panel"
import type { WorkflowBranchTargetOption } from "./node-config-panel"
@@ -250,8 +251,8 @@ export function WorkflowEditor({
[draft, nodeSpecs, propertyPanelNode]
)
const propertyPanelBranchSummaries = useMemo(
() => (propertyPanelNode ? getBranchSummaries(nodes, propertyPanelNode.id) : []),
[nodes, propertyPanelNode]
() => (propertyPanelNode ? getBranchSummaries(nodes, propertyPanelNode.id, nodeSpecs) : []),
[nodeSpecs, nodes, propertyPanelNode]
)
const propertyPanelBranchTargetOptions = useMemo(
() => (propertyPanelNode ? getBranchTargetOptions(nodes, edges, propertyPanelNode.id) : []),
@@ -1012,7 +1013,9 @@ const conditionOperators = [
{ value: "exists", label: "存在" },
{ value: "not_exists", label: "不存在" },
{ value: "truthy", label: "为真" },
{ value: "is_true", label: "为真" },
{ value: "falsy", label: "为假" },
{ value: "is_false", label: "为假" },
{ value: "gt", label: "大于" },
{ value: "gte", label: "大于等于" },
{ value: "lt", label: "小于" },
@@ -1021,7 +1024,8 @@ const conditionOperators = [
function getBranchSummaries(
nodes: WorkflowFlowNode[],
nodeId: string
nodeId: string,
nodeSpecs: AIWorkflowNodeSpec[]
): WorkflowBranchSummary[] {
const node = nodes.find((item) => item.id === nodeId)
const branches = node?.data.config?.branches ?? []
@@ -1032,7 +1036,7 @@ function getBranchSummaries(
branchId: branch.id,
targetNodeId: branch.targetNodeId,
targetName: target?.data.name ?? target?.data.title ?? branch.targetNodeId,
conditionLabel: branch.condition ? formatConditionLabel(branch.condition) : "无条件匹配",
conditionLabel: branch.condition ? formatConditionLabel(branch.condition, nodes, nodeSpecs) : "无条件匹配",
isDefault: Boolean(branch.default),
}
})
@@ -1054,31 +1058,62 @@ function getBranchTargetOptions(
})
}
function formatConditionLabel(condition: WorkflowCondition) {
const left = condition.left?.nodeId && condition.left.field
? `${condition.left.nodeId}.${condition.left.field}`
: "未选择变量"
function formatConditionLabel(
condition: WorkflowCondition,
nodes: WorkflowFlowNode[],
nodeSpecs: AIWorkflowNodeSpec[]
) {
const variable = findConditionOutputSpec(condition.left, nodes, nodeSpecs)
const left = variable?.label
?? (condition.left?.nodeId && condition.left.field ? `${condition.left.nodeId}.${condition.left.field}` : "未选择变量")
const operator = conditionOperators.find((item) => item.value === condition.operator)?.label
?? condition.operator
?? "未选择判断方式"
if (["exists", "not_exists", "truthy", "falsy"].includes(condition.operator ?? "")) {
if (["exists", "not_exists", "truthy", "is_true", "falsy", "is_false"].includes(condition.operator ?? "")) {
return `${left} ${operator}`
}
return `${left} ${operator} ${formatConditionRight(condition.right)}`
return `${left} ${operator} ${formatConditionRight(condition.right, variable)}`
}
function formatConditionRight(value: unknown) {
function formatConditionRight(value: unknown, variable?: WorkflowVariableSpec) {
if (value === undefined || value === null || value === "") {
return "未填写比较值"
}
const option = variable?.valueOptions?.find((item) => conditionValueEquals(item.value, value))
if (option) {
return option.label
}
if (variable?.type === "boolean") {
return value === true ? "是" : "否"
}
if (typeof value === "object") {
return JSON.stringify(value)
}
return String(value)
}
function findConditionOutputSpec(
selector: WorkflowCondition["left"],
nodes: WorkflowFlowNode[],
nodeSpecs: AIWorkflowNodeSpec[]
): WorkflowVariableSpec | undefined {
if (!selector?.nodeId || !selector.field) {
return undefined
}
const sourceNode = nodes.find((item) => item.id === selector.nodeId)
if (!sourceNode) {
return undefined
}
const spec = getNodeSpec(nodeSpecs, sourceNode.data.nodeType ?? "")
return spec?.outputSchema?.find((item) => item.name === selector.field)
}
function conditionValueEquals(left: unknown, right: unknown) {
return JSON.stringify(left) === JSON.stringify(right)
}
function getEventClientPoint(event: MouseEvent | TouchEvent) {
if ("changedTouches" in event) {
const touch = event.changedTouches[0] ?? event.touches[0]
@@ -317,6 +317,49 @@ describe("getAvailableVariables", () => {
assert.deepEqual(plain(variables), [])
})
it("preserves condition editor metadata from output specs", async () => {
const { getAvailableVariables } = await loadModule()
const variables = getAvailableVariables(
{
nodes: [
{ id: "policy_1", type: "workflowNode", position: { x: 0, y: 0 }, data: { nodeType: "reply_policy", name: "回复策略" } },
{ id: "condition_1", type: "workflowNode", position: { x: 200, y: 0 }, data: { nodeType: "condition" } },
],
edges: [{ id: "e1", source: "policy_1", target: "condition_1" }],
},
"condition_1",
[
{
type: "reply_policy",
outputSchema: [
{
name: "action",
label: "处理策略",
type: "string",
description: "Selected policy action.",
operators: ["eq", "neq"],
valueOptions: [{ value: "direct_reply", label: "直接回复客户" }],
},
],
},
]
)
assert.deepEqual(plain(variables), [
{
nodeId: "policy_1",
nodeName: "回复策略",
field: "action",
label: "处理策略",
type: "string",
description: "Selected policy action.",
operators: ["eq", "neq"],
valueOptions: [{ value: "direct_reply", label: "直接回复客户" }],
},
])
})
})
describe("toApiDefinition", () => {
@@ -108,11 +108,20 @@ export type WorkflowVariableSelector = {
field: string
}
export type WorkflowVariableValueOption = {
value: unknown
label: string
description?: string
}
export type WorkflowVariableSpec = {
name: string
label?: string
type: WorkflowVariableType
required?: boolean
description?: string
operators?: string[]
valueOptions?: WorkflowVariableValueOption[]
}
export type WorkflowNodeSpec = {
@@ -128,8 +137,11 @@ export type WorkflowVariableRef = {
nodeId: string
nodeName: string
field: string
label?: string
type: string
description: string
operators?: string[]
valueOptions?: WorkflowVariableValueOption[]
}
export type WorkflowDraftValidation = {
@@ -647,8 +659,11 @@ export function getAvailableVariables(
nodeId: sourceNode.id,
nodeName: sourceNode.data?.name ?? spec?.title ?? sourceNode.id,
field: output.name,
label: output.label,
type: output.type,
description: output.description ?? "",
operators: output.operators,
valueOptions: output.valueOptions,
})
}
}
+7
View File
@@ -301,9 +301,16 @@ export type AIWorkflowVariableSelector = {
export type AIWorkflowVariableSpec = {
name: string
label?: string
type: AIWorkflowVariableType
required?: boolean
description: string
operators?: string[]
valueOptions?: {
value: unknown
label: string
description?: string
}[]
}
export type AIWorkflowDefinition = {
+19
View File
@@ -0,0 +1,19 @@
import { request } from "@/lib/api/client"
export type PublicConfig = {
language: string
wxworkEnabled: boolean
oidcEnabled: boolean
}
let publicConfigPromise: Promise<PublicConfig> | null = null
export function fetchPublicConfig() {
publicConfigPromise ??= request<PublicConfig>("/api/config", {
skipAuth: true,
}).catch((error) => {
publicConfigPromise = null
throw error
})
return publicConfigPromise
}