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
+67 -7
View File
@@ -44,8 +44,25 @@ func DefaultRegistry() *Registry {
},
OutputSchema: []VariableSpec{
output("normalizedMessage", VariableTypeString, "Normalized customer message."),
output("messageIntent", VariableTypeString, "Detected customer message intent."),
output("answerScope", VariableTypeString, "Recommended answer scope."),
enumOutput("messageIntent", "消息意图", "Detected customer message intent.", []VariableValueOption{
valueOption("unknown", "未知意图", "系统暂时无法判断客户意图。"),
valueOption("greeting", "打招呼", "客户在问候或开始对话。"),
valueOption("thanks", "表达感谢", "客户在表示感谢。"),
valueOption("end_conversation", "结束会话", "客户表示问题已处理或准备结束。"),
valueOption("confirmation", "确认操作", "客户对上一步操作进行确认。"),
valueOption("handoff_request", "要求人工", "客户明确要求转人工处理。"),
valueOption("complaint", "投诉升级", "客户表达投诉、举报、起诉等升级风险。"),
valueOption("ticket_request", "要求建单", "客户希望创建或跟进工单。"),
valueOption("ambiguous_question", "问题不明确", "客户问题缺少必要上下文,需要追问。"),
valueOption("business_question", "业务问题", "客户问题适合进入知识库检索。"),
}),
enumOutput("answerScope", "回复策略", "Recommended answer scope.", []VariableValueOption{
valueOption("direct_reply", "直接回复客户", "无需检索知识库或转人工,可以直接生成回复。"),
valueOption("needs_clarification", "追问补充信息", "当前信息不足,需要客户补充。"),
valueOption("needs_handoff", "转人工处理", "需要人工客服介入。"),
valueOption("needs_ticket", "创建工单", "需要进入工单处理流程。"),
valueOption("needs_knowledge", "检索知识库", "需要先检索知识库再回答。"),
}),
output("confidence", VariableTypeNumber, "Classifier confidence."),
output("riskSignals", VariableTypeStringArray, "Detected risk signals."),
output("reason", VariableTypeString, "Decision reason."),
@@ -67,12 +84,31 @@ func DefaultRegistry() *Registry {
optionalInput("answerability", VariableTypeString, "Knowledge answerability decision."),
},
OutputSchema: []VariableSpec{
output("action", VariableTypeString, "Selected policy action."),
enumOutput("action", "处理策略", "Selected policy action.", []VariableValueOption{
valueOption("direct_reply", "直接回复客户", "直接发送策略节点生成的回复。"),
valueOption("clarify", "追问补充信息", "先让客户补充必要信息。"),
valueOption("end_conversation", "结束会话", "发送结束语并结束本轮处理。"),
valueOption("handoff_to_human", "转人工", "进入人工接待流程。"),
valueOption("prepare_ticket", "创建工单", "整理工单草稿并等待确认。"),
valueOption("retrieve_knowledge", "检索知识库", "进入知识检索和 AI 回复流程。"),
valueOption("knowledge_fallback", "知识库兜底", "知识库结果不足,发送兜底回复。"),
}),
output("replyText", VariableTypeString, "Customer-visible reply text when the policy can answer directly."),
output("reason", VariableTypeString, "Policy decision reason."),
output("requiresFlow", VariableTypeBoolean, "Whether the decision should continue into workflow actions."),
output("targetFlow", VariableTypeString, "Suggested target flow."),
output("finalReplySource", VariableTypeString, "Source category for the final reply."),
enumOutput("targetFlow", "目标流程", "Suggested target flow.", []VariableValueOption{
valueOption("handoff_to_human", "转人工流程", "继续执行转人工节点。"),
valueOption("prepare_ticket", "工单流程", "继续执行工单草稿和确认节点。"),
valueOption("knowledge", "知识库流程", "继续执行知识检索节点。"),
}),
enumOutput("finalReplySource", "回复来源", "Source category for the final reply.", []VariableValueOption{
valueOption("direct_reply", "策略直接回复", "由回复策略节点直接生成回复。"),
valueOption("clarification", "追问回复", "用于追问客户补充信息。"),
valueOption("handoff_notice", "转人工提示", "用于提示客户已进入人工处理。"),
valueOption("ticket_result", "工单结果", "用于提示建单结果。"),
valueOption("knowledge_answer", "知识库回答", "用于发送基于知识库生成的回复。"),
valueOption("knowledge_fallback", "知识库兜底", "用于知识库信息不足时的兜底回复。"),
}),
},
},
NodeSpec{
@@ -101,7 +137,10 @@ func DefaultRegistry() *Registry {
requiredInput("knowledgeItems", VariableTypeObjectArray, "Retrieved knowledge items."),
},
OutputSchema: []VariableSpec{
output("answerability", VariableTypeString, "Answerability decision."),
enumOutput("answerability", "可回答性", "Answerability decision.", []VariableValueOption{
valueOption("answerable", "可以回答", "检索结果足够支撑回答。"),
valueOption("unanswerable", "无法回答", "检索结果不足,应该走兜底或追问。"),
}),
output("reason", VariableTypeString, "Decision reason."),
},
},
@@ -197,7 +236,13 @@ func DefaultRegistry() *Registry {
OutputSchema: []VariableSpec{
output("handoffId", VariableTypeInteger, "Handoff operation ID."),
output("reason", VariableTypeString, "Handoff reason."),
output("decision", VariableTypeString, "Handoff dispatch decision."),
enumOutput("decision", "转人工结果", "Handoff dispatch decision.", []VariableValueOption{
valueOption("assigned", "已分配客服", "已成功分配给人工客服。"),
valueOption("team_pool", "团队队列等待", "暂未分配到客服,进入团队等待队列。"),
valueOption("global_pool", "全局队列等待", "非服务时间或无可用团队,进入全局等待队列。"),
valueOption("off_hours", "非服务时间", "当前不在人工客服服务时间内。"),
valueOption("cancelled", "已取消转人工", "由于未确认或条件不满足,未执行转人工。"),
}),
output("teamId", VariableTypeInteger, "Assigned or pending team ID."),
output("assigneeId", VariableTypeInteger, "Assigned agent user ID."),
output("message", VariableTypeString, "Customer-visible handoff notice."),
@@ -239,3 +284,18 @@ func optionalInput(name string, variableType VariableType, description string) V
func output(name string, variableType VariableType, description string) VariableSpec {
return VariableSpec{Name: name, Type: variableType, Description: description}
}
func enumOutput(name string, label string, description string, options []VariableValueOption) VariableSpec {
return VariableSpec{
Name: name,
Label: label,
Type: VariableTypeString,
Description: description,
Operators: []string{"eq", "neq"},
ValueOptions: options,
}
}
func valueOption(value any, label string, description string) VariableValueOption {
return VariableValueOption{Value: value, Label: label, Description: description}
}
+9
View File
@@ -26,9 +26,18 @@ const (
type VariableSpec struct {
Name string `json:"name"`
Label string `json:"label,omitempty"`
Type VariableType `json:"type"`
Required bool `json:"required,omitempty"`
Description string `json:"description"`
Operators []string `json:"operators,omitempty"`
ValueOptions []VariableValueOption `json:"valueOptions,omitempty"`
}
type VariableValueOption struct {
Value any `json:"value"`
Label string `json:"label"`
Description string `json:"description,omitempty"`
}
type NodeSpec struct {
+65 -1
View File
@@ -357,8 +357,17 @@ func (v *definitionValidator) validateCondition(field string, sourceNodeID strin
if !ok {
return
}
if _, ok := findOutputSpec(sourceSpec.OutputSchema, sourceField); !ok {
outputSpec, ok := findOutputSpec(sourceSpec.OutputSchema, sourceField)
if !ok {
v.addError(field+".left", "condition source field does not exist: "+sourceSelectorNodeID+"."+sourceField)
return
}
if len(outputSpec.Operators) > 0 && !stringInSlice(outputSpec.Operators, operator) {
v.addError(field+".operator", "condition operator is not allowed for variable: "+operator)
return
}
if !conditionOperatorWithoutRight(operator) && len(outputSpec.ValueOptions) > 0 && !valueOptionExists(outputSpec.ValueOptions, condition.Right) {
v.addError(field+".right", "condition comparison value is not allowed")
}
}
@@ -371,6 +380,61 @@ func isSupportedConditionOperator(operator string) bool {
}
}
func conditionOperatorWithoutRight(operator string) bool {
switch strings.TrimSpace(operator) {
case "exists", "not_exists", "truthy", "is_true", "falsy", "is_false":
return true
default:
return false
}
}
func stringInSlice(items []string, value string) bool {
for _, item := range items {
if strings.TrimSpace(item) == value {
return true
}
}
return false
}
func valueOptionExists(items []registry.VariableValueOption, value any) bool {
for _, item := range items {
if conditionValuesEqual(item.Value, value) {
return true
}
}
return false
}
func conditionValuesEqual(left any, right any) bool {
switch l := left.(type) {
case string:
r, ok := right.(string)
return ok && l == r
case bool:
r, ok := right.(bool)
return ok && l == r
case int:
return conditionValuesEqual(float64(l), right)
case int64:
return conditionValuesEqual(float64(l), right)
case float64:
switch r := right.(type) {
case int:
return l == float64(r)
case int64:
return l == float64(r)
case float64:
return l == r
default:
return false
}
default:
return false
}
}
func (v *definitionValidator) hasPath(sourceID string, targetID string, visiting map[string]struct{}) bool {
if sourceID == targetID {
return false
@@ -304,6 +304,19 @@ func TestValidateDefinitionRejectsUnknownConditionVariable(t *testing.T) {
}
}
func TestValidateDefinitionRejectsInvalidConditionEnumValue(t *testing.T) {
def := policyConditionDefinition("unknown_action")
result := validator.ValidateDefinition(def, registry.DefaultRegistry())
if result.Valid {
t.Fatalf("expected invalid enum condition value to be rejected")
}
if !hasValidationMessage(result, "condition comparison value is not allowed") {
t.Fatalf("expected condition enum value error, got %#v", result.Errors)
}
}
func minimalDefinition() dsl.Definition {
return dsl.Definition{
SchemaVersion: 1,
@@ -322,6 +335,52 @@ func minimalDefinition() dsl.Definition {
}
}
func policyConditionDefinition(action any) dsl.Definition {
conditionConfig, _ := json.Marshal(dsl.ConditionConfig{
Branches: []dsl.ConditionBranch{
{
ID: "direct",
Name: "Direct",
TargetNodeID: "end_1",
Condition: &dsl.Condition{
Left: &dsl.VariableSelector{NodeID: "policy_1", Field: "action"},
Operator: "eq",
Right: action,
},
},
{
ID: "default",
Name: "Default",
TargetNodeID: "end_1",
Default: true,
},
},
})
return dsl.Definition{
SchemaVersion: 1,
EntryNodeID: "start_1",
Nodes: []dsl.Node{
{ID: "start_1", Type: "start"},
{ID: "understanding_1", Type: "conversation_understanding", Inputs: map[string]dsl.VariableSelector{
"userMessage": {NodeID: "start_1", Field: "userMessage"},
}},
{ID: "policy_1", Type: "reply_policy", Inputs: map[string]dsl.VariableSelector{
"messageIntent": {NodeID: "understanding_1", Field: "messageIntent"},
"answerScope": {NodeID: "understanding_1", Field: "answerScope"},
}},
{ID: "condition_1", Type: "condition", Config: conditionConfig},
{ID: "end_1", Type: "end"},
},
Edges: []dsl.Edge{
{ID: "e1", Source: "start_1", Target: "understanding_1"},
{ID: "e2", Source: "understanding_1", Target: "policy_1"},
{ID: "e3", Source: "policy_1", Target: "condition_1"},
{ID: "e4", Source: "condition_1", Target: "end_1"},
{ID: "e5", Source: "condition_1", Target: "end_1"},
},
}
}
func mappedReplyDefinition() dsl.Definition {
def := minimalDefinition()
def.Nodes[1].Inputs = map[string]dsl.VariableSelector{
@@ -34,6 +34,36 @@ func TestBuildAIWorkflowNodeSpecsIncludesVariableContracts(t *testing.T) {
}
}
func TestBuildAIWorkflowNodeSpecsIncludesConditionValueOptions(t *testing.T) {
specs := BuildAIWorkflowNodeSpecs(workflowregistry.DefaultRegistry().List())
var action *workflowregistry.VariableSpec
for _, spec := range specs {
if spec.Type != workflowregistry.NodeTypeReplyPolicy {
continue
}
for index := range spec.OutputSchema {
if spec.OutputSchema[index].Name == "action" {
action = &spec.OutputSchema[index]
break
}
}
}
if action == nil {
t.Fatalf("expected reply_policy action output")
}
if action.Label != "处理策略" {
t.Fatalf("expected user-facing action label, got %q", action.Label)
}
if !hasResponseVariableOption(action.ValueOptions, "direct_reply", "直接回复客户") {
t.Fatalf("expected direct_reply option with business label, got %#v", action.ValueOptions)
}
if !hasResponseOperator(action.Operators, "eq") || !hasResponseOperator(action.Operators, "neq") {
t.Fatalf("expected action to constrain condition operators, got %#v", action.Operators)
}
}
func TestBuildAIWorkflowRunIncludesAuditDisplayFields(t *testing.T) {
startedAt := time.Date(2026, 6, 23, 10, 0, 0, 0, time.UTC)
endedAt := startedAt.Add(1500 * time.Millisecond)
@@ -109,3 +139,21 @@ func hasResponseVariable(items []workflowregistry.VariableSpec, name string) boo
}
return false
}
func hasResponseVariableOption(items []workflowregistry.VariableValueOption, value any, label string) bool {
for _, item := range items {
if item.Value == value && item.Label == label {
return true
}
}
return false
}
func hasResponseOperator(items []string, value string) bool {
for _, item := range items {
if item == value {
return true
}
}
return false
}
@@ -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
<ConditionRightControl
value={conditionRight}
onChange={(event) => commitBranch(branch.id, {
condition: {
...condition,
right: normalizeConditionRight(event.target.value),
},
variable={selectedVariable}
onChange={(right) => commitBranch(branch.id, {
condition: { ...condition, right },
})}
placeholder="请输入比较值"
/>
</div>
) : 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>
)
}
function normalizeConditionRight(value: string) {
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 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
}