feat: enhance variable handling and UI components with business labels and descriptions

This commit is contained in:
mlogclub
2026-06-29 18:37:31 +08:00
parent 295c7b4bea
commit 87760d5e89
8 changed files with 166 additions and 85 deletions
+68 -68
View File
@@ -28,11 +28,11 @@ func DefaultRegistry() *Registry {
Icon: "PlayCircleIcon", Icon: "PlayCircleIcon",
RiskLevel: NodeRiskLevelLow, RiskLevel: NodeRiskLevelLow,
OutputSchema: []VariableSpec{ OutputSchema: []VariableSpec{
output("conversationId", VariableTypeInteger, "Conversation ID."), output("conversationId", "会话 ID", VariableTypeInteger, "当前客户会话的内部编号。"),
output("messageId", VariableTypeInteger, "Current user message ID."), output("messageId", "消息 ID", VariableTypeInteger, "客户本轮消息的内部编号。"),
output("aiAgentId", VariableTypeInteger, "AI Agent ID."), output("aiAgentId", "AI Agent ID", VariableTypeInteger, "当前处理会话的 AI Agent 编号。"),
output("userMessage", VariableTypeString, "Current user message content."), output("userMessage", "用户消息", VariableTypeString, "客户本轮发送的原始消息内容。"),
output("knowledgeBaseIds", VariableTypeIntegerArray, "Knowledge bases bound to the AI Agent."), output("knowledgeBaseIds", "知识库 ID 列表", VariableTypeIntegerArray, "当前 AI Agent 已绑定的知识库编号列表。"),
}, },
}, },
NodeSpec{ NodeSpec{
@@ -42,11 +42,11 @@ func DefaultRegistry() *Registry {
Icon: "MessageCircleIcon", Icon: "MessageCircleIcon",
RiskLevel: NodeRiskLevelLow, RiskLevel: NodeRiskLevelLow,
InputSchema: []VariableSpec{ InputSchema: []VariableSpec{
requiredInput("userMessage", VariableTypeString, "Current user message content."), requiredInput("userMessage", "用户消息", VariableTypeString, "客户本轮发送的原始消息内容。"),
}, },
OutputSchema: []VariableSpec{ OutputSchema: []VariableSpec{
output("normalizedMessage", VariableTypeString, "Normalized customer message."), output("normalizedMessage", "规范化消息", VariableTypeString, "经过清洗和规范化后的客户消息。"),
enumOutput("messageIntent", "消息意图", "Detected customer message intent.", []VariableValueOption{ enumOutput("messageIntent", "消息意图", "客户消息的意图分类。", []VariableValueOption{
valueOption("unknown", "未知意图", "系统暂时无法判断客户意图。"), valueOption("unknown", "未知意图", "系统暂时无法判断客户意图。"),
valueOption("greeting", "打招呼", "客户在问候或开始对话。"), valueOption("greeting", "打招呼", "客户在问候或开始对话。"),
valueOption("thanks", "表达感谢", "客户在表示感谢。"), valueOption("thanks", "表达感谢", "客户在表示感谢。"),
@@ -58,16 +58,16 @@ func DefaultRegistry() *Registry {
valueOption("ambiguous_question", "问题不明确", "客户问题缺少必要上下文,需要追问。"), valueOption("ambiguous_question", "问题不明确", "客户问题缺少必要上下文,需要追问。"),
valueOption("business_question", "业务问题", "客户问题适合进入知识库检索。"), valueOption("business_question", "业务问题", "客户问题适合进入知识库检索。"),
}), }),
enumOutput("answerScope", "回复策略", "Recommended answer scope.", []VariableValueOption{ enumOutput("answerScope", "回复策略", "系统建议采用的回复处理范围。", []VariableValueOption{
valueOption("direct_reply", "直接回复客户", "无需检索知识库或转人工,可以直接生成回复。"), valueOption("direct_reply", "直接回复客户", "无需检索知识库或转人工,可以直接生成回复。"),
valueOption("needs_clarification", "追问补充信息", "当前信息不足,需要客户补充。"), valueOption("needs_clarification", "追问补充信息", "当前信息不足,需要客户补充。"),
valueOption("needs_handoff", "转人工处理", "需要人工客服介入。"), valueOption("needs_handoff", "转人工处理", "需要人工客服介入。"),
valueOption("needs_ticket", "创建工单", "需要进入工单处理流程。"), valueOption("needs_ticket", "创建工单", "需要进入工单处理流程。"),
valueOption("needs_knowledge", "检索知识库", "需要先检索知识库再回答。"), valueOption("needs_knowledge", "检索知识库", "需要先检索知识库再回答。"),
}), }),
output("confidence", VariableTypeNumber, "Classifier confidence."), output("confidence", "置信度", VariableTypeNumber, "意图和回复策略判断的置信度。"),
output("riskSignals", VariableTypeStringArray, "Detected risk signals."), output("riskSignals", "风险信号", VariableTypeStringArray, "识别到的投诉、升级、人工介入等风险线索。"),
output("reason", VariableTypeString, "Decision reason."), output("reason", "判断原因", VariableTypeString, "本次意图和回复策略判断的原因说明。"),
}, },
DefaultInputs: map[string]dsl.Value{ DefaultInputs: map[string]dsl.Value{
"userMessage": dsl.RefValue("start_1", "userMessage"), "userMessage": dsl.RefValue("start_1", "userMessage"),
@@ -80,14 +80,14 @@ func DefaultRegistry() *Registry {
Icon: "ShieldCheckIcon", Icon: "ShieldCheckIcon",
RiskLevel: NodeRiskLevelLow, RiskLevel: NodeRiskLevelLow,
InputSchema: []VariableSpec{ InputSchema: []VariableSpec{
requiredInput("messageIntent", VariableTypeString, "Detected customer message intent."), requiredInput("messageIntent", "消息意图", VariableTypeString, "上游理解节点识别出的客户消息意图。"),
requiredInput("answerScope", VariableTypeString, "Recommended answer scope."), requiredInput("answerScope", "回复策略", VariableTypeString, "上游理解节点建议采用的回复处理范围。"),
optionalInput("userMessage", VariableTypeString, "Current user message content."), optionalInput("userMessage", "用户消息", VariableTypeString, "客户本轮发送的原始消息内容。"),
optionalInput("riskSignals", VariableTypeStringArray, "Detected risk signals."), optionalInput("riskSignals", "风险信号", VariableTypeStringArray, "上游识别到的风险线索列表。"),
optionalInput("answerability", VariableTypeString, "Knowledge answerability decision."), optionalInput("answerability", "可回答性", VariableTypeString, "知识库结果是否足够支撑回答的判断。"),
}, },
OutputSchema: []VariableSpec{ OutputSchema: []VariableSpec{
enumOutput("action", "处理策略", "Selected policy action.", []VariableValueOption{ enumOutput("action", "处理策略", "回复策略节点选择的下一步处理动作。", []VariableValueOption{
valueOption("direct_reply", "直接回复客户", "直接发送策略节点生成的回复。"), valueOption("direct_reply", "直接回复客户", "直接发送策略节点生成的回复。"),
valueOption("clarify", "追问补充信息", "先让客户补充必要信息。"), valueOption("clarify", "追问补充信息", "先让客户补充必要信息。"),
valueOption("end_conversation", "结束会话", "发送结束语并结束本轮处理。"), valueOption("end_conversation", "结束会话", "发送结束语并结束本轮处理。"),
@@ -96,15 +96,15 @@ func DefaultRegistry() *Registry {
valueOption("retrieve_knowledge", "检索知识库", "进入知识检索和 AI 回复流程。"), valueOption("retrieve_knowledge", "检索知识库", "进入知识检索和 AI 回复流程。"),
valueOption("knowledge_fallback", "知识库兜底", "知识库结果不足,发送兜底回复。"), valueOption("knowledge_fallback", "知识库兜底", "知识库结果不足,发送兜底回复。"),
}), }),
output("replyText", VariableTypeString, "Customer-visible reply text when the policy can answer directly."), output("replyText", "回复内容", VariableTypeString, "可直接发送给客户的回复文本。"),
output("reason", VariableTypeString, "Policy decision reason."), output("reason", "策略原因", VariableTypeString, "选择当前处理策略的原因说明。"),
output("requiresFlow", VariableTypeBoolean, "Whether the decision should continue into workflow actions."), output("requiresFlow", "需要继续流程", VariableTypeBoolean, "是否需要继续执行后续工作流节点。"),
enumOutput("targetFlow", "目标流程", "Suggested target flow.", []VariableValueOption{ enumOutput("targetFlow", "目标流程", "建议继续执行的业务流程。", []VariableValueOption{
valueOption("handoff_to_human", "转人工流程", "继续执行转人工节点。"), valueOption("handoff_to_human", "转人工流程", "继续执行转人工节点。"),
valueOption("prepare_ticket", "工单流程", "继续执行工单草稿和确认节点。"), valueOption("prepare_ticket", "工单流程", "继续执行工单草稿和确认节点。"),
valueOption("knowledge", "知识库流程", "继续执行知识检索节点。"), valueOption("knowledge", "知识库流程", "继续执行知识检索节点。"),
}), }),
enumOutput("finalReplySource", "回复来源", "Source category for the final reply.", []VariableValueOption{ enumOutput("finalReplySource", "回复来源", "最终回复内容的来源类别。", []VariableValueOption{
valueOption("direct_reply", "策略直接回复", "由回复策略节点直接生成回复。"), valueOption("direct_reply", "策略直接回复", "由回复策略节点直接生成回复。"),
valueOption("clarification", "追问回复", "用于追问客户补充信息。"), valueOption("clarification", "追问回复", "用于追问客户补充信息。"),
valueOption("handoff_notice", "转人工提示", "用于提示客户已进入人工处理。"), valueOption("handoff_notice", "转人工提示", "用于提示客户已进入人工处理。"),
@@ -121,11 +121,11 @@ func DefaultRegistry() *Registry {
Icon: "BookOpenIcon", Icon: "BookOpenIcon",
RiskLevel: NodeRiskLevelLow, RiskLevel: NodeRiskLevelLow,
InputSchema: []VariableSpec{ InputSchema: []VariableSpec{
requiredInput("query", VariableTypeString, "Search query."), requiredInput("query", "检索问题", VariableTypeString, "用于检索知识库的客户问题或查询文本。"),
}, },
OutputSchema: []VariableSpec{ OutputSchema: []VariableSpec{
output("items", VariableTypeObjectArray, "Retrieved knowledge items."), output("items", "知识条目", VariableTypeObjectArray, "从知识库命中的原始知识条目列表。"),
output("summary", VariableTypeString, "Short retrieval summary."), output("summary", "检索摘要", VariableTypeString, "对本次知识检索结果的简短摘要。"),
}, },
DefaultInputs: map[string]dsl.Value{ DefaultInputs: map[string]dsl.Value{
"query": dsl.RefValue("start_1", "userMessage"), "query": dsl.RefValue("start_1", "userMessage"),
@@ -138,15 +138,15 @@ func DefaultRegistry() *Registry {
Icon: "HelpCircleIcon", Icon: "HelpCircleIcon",
RiskLevel: NodeRiskLevelLow, RiskLevel: NodeRiskLevelLow,
InputSchema: []VariableSpec{ InputSchema: []VariableSpec{
requiredInput("userMessage", VariableTypeString, "Current user message content."), requiredInput("userMessage", "用户消息", VariableTypeString, "客户本轮发送的原始消息内容。"),
requiredInput("knowledgeItems", VariableTypeObjectArray, "Retrieved knowledge items."), requiredInput("knowledgeItems", "知识条目", VariableTypeObjectArray, "上游知识检索节点命中的知识条目列表。"),
}, },
OutputSchema: []VariableSpec{ OutputSchema: []VariableSpec{
enumOutput("answerability", "可回答性", "Answerability decision.", []VariableValueOption{ enumOutput("answerability", "可回答性", "知识库结果是否足够支撑回答的判断。", []VariableValueOption{
valueOption("answerable", "可以回答", "检索结果足够支撑回答。"), valueOption("answerable", "可以回答", "检索结果足够支撑回答。"),
valueOption("unanswerable", "无法回答", "检索结果不足,应该走兜底或追问。"), valueOption("unanswerable", "无法回答", "检索结果不足,应该走兜底或追问。"),
}), }),
output("reason", VariableTypeString, "Decision reason."), output("reason", "判断原因", VariableTypeString, "可回答性判断的原因说明。"),
}, },
}, },
NodeSpec{ NodeSpec{
@@ -156,11 +156,11 @@ func DefaultRegistry() *Registry {
Icon: "BotIcon", Icon: "BotIcon",
RiskLevel: NodeRiskLevelMedium, RiskLevel: NodeRiskLevelMedium,
InputSchema: []VariableSpec{ InputSchema: []VariableSpec{
requiredInput("userMessage", VariableTypeString, "Current user message content."), requiredInput("userMessage", "用户消息", VariableTypeString, "客户本轮发送的原始消息内容。"),
optionalInput("knowledgeItems", VariableTypeObjectArray, "Retrieved knowledge items."), optionalInput("knowledgeItems", "知识条目", VariableTypeObjectArray, "可用于生成回复的知识库检索结果。"),
}, },
OutputSchema: []VariableSpec{ OutputSchema: []VariableSpec{
output("replyText", VariableTypeString, "Generated reply text."), output("replyText", "回复内容", VariableTypeString, "大模型生成的客户可见回复文本。"),
}, },
}, },
NodeSpec{ NodeSpec{
@@ -170,7 +170,7 @@ func DefaultRegistry() *Registry {
Icon: "GitBranchIcon", Icon: "GitBranchIcon",
RiskLevel: NodeRiskLevelLow, RiskLevel: NodeRiskLevelLow,
OutputSchema: []VariableSpec{ OutputSchema: []VariableSpec{
output("matched", VariableTypeBoolean, "Whether the condition matched."), output("matched", "是否命中", VariableTypeBoolean, "条件节点是否命中了某个条件分支。"),
}, },
}, },
NodeSpec{ NodeSpec{
@@ -180,13 +180,13 @@ func DefaultRegistry() *Registry {
Icon: "SearchIcon", Icon: "SearchIcon",
RiskLevel: NodeRiskLevelLow, RiskLevel: NodeRiskLevelLow,
InputSchema: []VariableSpec{ InputSchema: []VariableSpec{
requiredInput("userMessage", VariableTypeString, "Current user message content."), requiredInput("userMessage", "用户消息", VariableTypeString, "客户本轮发送的原始消息内容。"),
}, },
OutputSchema: []VariableSpec{ OutputSchema: []VariableSpec{
output("intent", VariableTypeString, "Detected user intent."), output("intent", "用户意图", VariableTypeString, "从会话中识别出的客户意图。"),
output("riskLevel", VariableTypeString, "Detected risk level."), output("riskLevel", "风险等级", VariableTypeString, "本轮会话的风险等级判断。"),
output("needTicket", VariableTypeBoolean, "Whether a ticket is recommended."), output("needTicket", "需要工单", VariableTypeBoolean, "是否建议进入工单处理流程。"),
output("needHumanHandoff", VariableTypeBoolean, "Whether human handoff is recommended."), output("needHumanHandoff", "需要转人工", VariableTypeBoolean, "是否建议转人工客服处理。"),
}, },
}, },
NodeSpec{ NodeSpec{
@@ -196,10 +196,10 @@ func DefaultRegistry() *Registry {
Icon: "ClipboardListIcon", Icon: "ClipboardListIcon",
RiskLevel: NodeRiskLevelMedium, RiskLevel: NodeRiskLevelMedium,
InputSchema: []VariableSpec{ InputSchema: []VariableSpec{
requiredInput("issue", VariableTypeString, "Issue summary."), requiredInput("issue", "问题摘要", VariableTypeString, "需要整理进工单的客户问题摘要。"),
}, },
OutputSchema: []VariableSpec{ OutputSchema: []VariableSpec{
output("ticketDraft", VariableTypeObject, "Draft ticket payload."), output("ticketDraft", "工单草稿", VariableTypeObject, "根据会话内容整理出的待确认工单草稿。"),
}, },
}, },
NodeSpec{ NodeSpec{
@@ -210,11 +210,11 @@ func DefaultRegistry() *Registry {
RiskLevel: NodeRiskLevelMedium, RiskLevel: NodeRiskLevelMedium,
Interruptible: true, Interruptible: true,
InputSchema: []VariableSpec{ InputSchema: []VariableSpec{
requiredInput("prompt", VariableTypeString, "Confirmation prompt."), requiredInput("prompt", "确认提示", VariableTypeString, "发送给客户用于确认操作的提示文本。"),
}, },
OutputSchema: []VariableSpec{ OutputSchema: []VariableSpec{
output("confirmed", VariableTypeBoolean, "Whether the user confirmed."), output("confirmed", "已确认", VariableTypeBoolean, "客户是否明确确认继续执行。"),
output("responseText", VariableTypeString, "Confirmation response text."), output("responseText", "确认回复", VariableTypeString, "客户针对确认提示给出的回复文本。"),
}, },
}, },
NodeSpec{ NodeSpec{
@@ -225,14 +225,14 @@ func DefaultRegistry() *Registry {
RiskLevel: NodeRiskLevelHigh, RiskLevel: NodeRiskLevelHigh,
RequiresConfirmationPredecessor: true, RequiresConfirmationPredecessor: true,
InputSchema: []VariableSpec{ InputSchema: []VariableSpec{
requiredInput("ticketDraft", VariableTypeObject, "Confirmed draft ticket payload."), requiredInput("ticketDraft", "工单草稿", VariableTypeObject, "已经由客户确认的工单草稿内容。"),
requiredInput("confirmed", VariableTypeBoolean, "Confirmation result."), requiredInput("confirmed", "已确认", VariableTypeBoolean, "客户是否已确认创建工单。"),
}, },
OutputSchema: []VariableSpec{ OutputSchema: []VariableSpec{
output("ticketId", VariableTypeInteger, "Created ticket ID."), output("ticketId", "工单 ID", VariableTypeInteger, "创建成功后的工单内部编号。"),
output("ticketNo", VariableTypeString, "Created ticket number."), output("ticketNo", "工单编号", VariableTypeString, "创建成功后的客户可见工单编号。"),
output("created", VariableTypeBoolean, "Whether the ticket was created."), output("created", "已创建", VariableTypeBoolean, "工单是否已经成功创建。"),
output("message", VariableTypeString, "Customer-visible ticket creation result."), output("message", "结果消息", VariableTypeString, "发送给客户的工单创建结果说明。"),
}, },
}, },
NodeSpec{ NodeSpec{
@@ -242,22 +242,22 @@ func DefaultRegistry() *Registry {
Icon: "HeadphonesIcon", Icon: "HeadphonesIcon",
RiskLevel: NodeRiskLevelHigh, RiskLevel: NodeRiskLevelHigh,
InputSchema: []VariableSpec{ InputSchema: []VariableSpec{
requiredInput("reason", VariableTypeString, "Handoff reason."), requiredInput("reason", "转人工原因", VariableTypeString, "触发转人工处理的业务原因。"),
optionalInput("confirmed", VariableTypeBoolean, "Confirmation result."), optionalInput("confirmed", "已确认", VariableTypeBoolean, "客户是否已确认转人工。"),
}, },
OutputSchema: []VariableSpec{ OutputSchema: []VariableSpec{
output("handoffId", VariableTypeInteger, "Handoff operation ID."), output("handoffId", "转人工记录 ID", VariableTypeInteger, "本次转人工操作的内部记录编号。"),
output("reason", VariableTypeString, "Handoff reason."), output("reason", "转人工原因", VariableTypeString, "本次转人工处理的原因说明。"),
enumOutput("decision", "转人工结果", "Handoff dispatch decision.", []VariableValueOption{ enumOutput("decision", "转人工结果", "转人工分配或排队结果。", []VariableValueOption{
valueOption("assigned", "已分配客服", "已成功分配给人工客服。"), valueOption("assigned", "已分配客服", "已成功分配给人工客服。"),
valueOption("team_pool", "团队队列等待", "暂未分配到客服,进入团队等待队列。"), valueOption("team_pool", "团队队列等待", "暂未分配到客服,进入团队等待队列。"),
valueOption("global_pool", "全局队列等待", "非服务时间或无可用团队,进入全局等待队列。"), valueOption("global_pool", "全局队列等待", "非服务时间或无可用团队,进入全局等待队列。"),
valueOption("off_hours", "非服务时间", "当前不在人工客服服务时间内。"), valueOption("off_hours", "非服务时间", "当前不在人工客服服务时间内。"),
valueOption("cancelled", "已取消转人工", "由于未确认或条件不满足,未执行转人工。"), valueOption("cancelled", "已取消转人工", "由于未确认或条件不满足,未执行转人工。"),
}), }),
output("teamId", VariableTypeInteger, "Assigned or pending team ID."), output("teamId", "客服组 ID", VariableTypeInteger, "已分配或等待中的客服组编号。"),
output("assigneeId", VariableTypeInteger, "Assigned agent user ID."), output("assigneeId", "客服 ID", VariableTypeInteger, "已分配的人工客服用户编号。"),
output("message", VariableTypeString, "Customer-visible handoff notice."), output("message", "转人工提示", VariableTypeString, "发送给客户的转人工结果提示。"),
}, },
}, },
NodeSpec{ NodeSpec{
@@ -267,11 +267,11 @@ func DefaultRegistry() *Registry {
Icon: "SendIcon", Icon: "SendIcon",
RiskLevel: NodeRiskLevelLow, RiskLevel: NodeRiskLevelLow,
InputSchema: []VariableSpec{ InputSchema: []VariableSpec{
requiredInput("replyText", VariableTypeString, "Customer-visible reply text."), requiredInput("replyText", "回复内容", VariableTypeString, "将发送或返回给客户的最终回复文本。"),
}, },
OutputSchema: []VariableSpec{ OutputSchema: []VariableSpec{
output("sent", VariableTypeBoolean, "Whether the reply was sent."), output("sent", "已发送", VariableTypeBoolean, "回复是否已经成功发送或返回。"),
output("replyMessageId", VariableTypeInteger, "Reply message ID."), output("replyMessageId", "回复消息 ID", VariableTypeInteger, "发送成功后的回复消息编号。"),
}, },
}, },
NodeSpec{ NodeSpec{
@@ -281,22 +281,22 @@ func DefaultRegistry() *Registry {
Icon: "FlagIcon", Icon: "FlagIcon",
RiskLevel: NodeRiskLevelLow, RiskLevel: NodeRiskLevelLow,
OutputSchema: []VariableSpec{ OutputSchema: []VariableSpec{
output("status", VariableTypeString, "Workflow terminal status."), output("status", "结束状态", VariableTypeString, "工作流执行结束时的状态。"),
}, },
}, },
) )
} }
func requiredInput(name string, variableType VariableType, description string) VariableSpec { func requiredInput(name string, label string, variableType VariableType, description string) VariableSpec {
return VariableSpec{Name: name, Type: variableType, Required: true, Description: description} return VariableSpec{Name: name, Label: label, Type: variableType, Required: true, Description: description}
} }
func optionalInput(name string, variableType VariableType, description string) VariableSpec { func optionalInput(name string, label string, variableType VariableType, description string) VariableSpec {
return VariableSpec{Name: name, Type: variableType, Description: description} return VariableSpec{Name: name, Label: label, Type: variableType, Description: description}
} }
func output(name string, variableType VariableType, description string) VariableSpec { func output(name string, label string, variableType VariableType, description string) VariableSpec {
return VariableSpec{Name: name, Type: variableType, Description: description} return VariableSpec{Name: name, Label: label, Type: variableType, Description: description}
} }
func enumOutput(name string, label string, description string, options []VariableValueOption) VariableSpec { func enumOutput(name string, label string, description string, options []VariableValueOption) VariableSpec {
@@ -2,6 +2,19 @@ package registry
import "testing" import "testing"
func TestDefaultRegistryVariablesHaveBusinessLabels(t *testing.T) {
for _, spec := range DefaultRegistry().List() {
for _, variable := range append(spec.InputSchema, spec.OutputSchema...) {
if variable.Label == "" {
t.Fatalf("%s.%s is missing business label", spec.Type, variable.Name)
}
if variable.Description == "" || variable.Description == variable.Name {
t.Fatalf("%s.%s is missing readable description", spec.Type, variable.Name)
}
}
}
}
func TestDefaultRegistryExposesStartOutputs(t *testing.T) { func TestDefaultRegistryExposesStartOutputs(t *testing.T) {
spec, ok := DefaultRegistry().Get(NodeTypeStart) spec, ok := DefaultRegistry().Get(NodeTypeStart)
if !ok { if !ok {
@@ -15,6 +15,8 @@ import {
createConditionBranchID, createConditionBranchID,
isRefValue, isRefValue,
normalizeNodeConfig, normalizeNodeConfig,
refField,
refNodeId,
type WorkflowConditionBranch, type WorkflowConditionBranch,
type WorkflowVariableRef, type WorkflowVariableRef,
} from "./workflow-utils" } from "./workflow-utils"
@@ -383,6 +385,11 @@ function ConditionFields({
compact?: boolean compact?: boolean
}) { }) {
const condition = branch.condition ?? {} const condition = branch.condition ?? {}
const selectedVariable = isRefValue(condition.left)
? variables.find((item) => item.nodeId === refNodeId(condition.left) && item.field === refField(condition.left))
: undefined
const valueOptions = selectedVariable?.valueOptions ?? []
const rightDisabled = ["exists", "empty"].includes(condition.operator ?? "")
return ( return (
<div className={cn("space-y-3", compact && "space-y-2")}> <div className={cn("space-y-3", compact && "space-y-2")}>
@@ -414,17 +421,37 @@ function ConditionFields({
})} })}
/> />
</div> </div>
<div className={cn("space-y-1.5", compact && "space-y-0", ["exists", "empty"].includes(condition.operator ?? "") && "opacity-50")}> <div className={cn("space-y-1.5", compact && "space-y-0", rightDisabled && "opacity-50")}>
{compact ? null : <Label className="text-xs text-slate-500"></Label>} {compact ? null : <Label className="text-xs text-slate-500"></Label>}
<Input {valueOptions.length > 0 && !rightDisabled ? (
value={stringifyConditionRight(condition.right)} <OptionCombobox
disabled={["exists", "empty"].includes(condition.operator ?? "")} value={stringifyConditionRight(condition.right)}
className={cardInputClassName} options={valueOptions.map((option) => ({
onChange={(event) => onChange({ value: stringifyConditionRight(option.value),
...branch, label: option.label || stringifyConditionRight(option.value),
condition: { ...condition, right: event.target.value }, description: option.description,
})} }))}
/> placeholder="选择取值"
triggerClassName={cardComboboxClassName}
onChange={(nextValue) => {
const selectedOption = valueOptions.find((option) => stringifyConditionRight(option.value) === nextValue)
onChange({
...branch,
condition: { ...condition, right: selectedOption?.value ?? nextValue },
})
}}
/>
) : (
<Input
value={stringifyConditionRight(condition.right)}
disabled={rightDisabled}
className={cardInputClassName}
onChange={(event) => onChange({
...branch,
condition: { ...condition, right: event.target.value },
})}
/>
)}
</div> </div>
</div> </div>
</div> </div>
@@ -3,6 +3,7 @@
import { OptionCombobox } from "@/components/option-combobox" import { OptionCombobox } from "@/components/option-combobox"
import { import {
buildVariableOption,
createRefValue, createRefValue,
refField, refField,
refNodeId, refNodeId,
@@ -24,11 +25,7 @@ export function VariableSelector({
triggerClassName?: string triggerClassName?: string
}) { }) {
const selected = value ? `${refNodeId(value)}.${refField(value)}` : "" const selected = value ? `${refNodeId(value)}.${refField(value)}` : ""
const options = variables.map((variable) => ({ const options = variables.map(buildVariableOption)
value: `${variable.nodeId}.${variable.field}`,
label: `${variable.nodeName}.${variable.label || variable.field}`,
description: variable.description,
}))
return ( return (
<OptionCombobox <OptionCombobox
@@ -158,7 +158,7 @@ export function WorkflowConfigPanel({
</Button> </Button>
</div> </div>
{panelDescription ? ( {panelDescription ? (
<div className="mt-1 pl-9 text-xs leading-5 text-slate-500"> <div className="mt-1 text-xs leading-5 text-slate-500">
{panelDescription} {panelDescription}
</div> </div>
) : null} ) : null}
@@ -226,6 +226,26 @@ describe("getAvailableVariables", () => {
}) })
}) })
describe("workflow variable display helpers", () => {
it("builds business-first variable options with technical details", async () => {
const { buildVariableOption } = await loadModule()
assert.deepEqual(plain(buildVariableOption({
nodeId: "start_1",
nodeName: "开始",
field: "userMessage",
label: "用户消息",
type: "string",
description: "客户本轮发送的消息内容",
})), {
value: "start_1.userMessage",
label: "开始 / 用户消息",
subtitle: "start_1.userMessage · string",
description: "客户本轮发送的消息内容",
})
})
})
describe("workflow definition mutations", () => { describe("workflow definition mutations", () => {
it("updates node data without changing unrelated nodes", async () => { it("updates node data without changing unrelated nodes", async () => {
const { updateWorkflowNodeData } = await loadModule() const { updateWorkflowNodeData } = await loadModule()
@@ -65,6 +65,13 @@ export type WorkflowVariableRef = {
valueOptions?: WorkflowVariableValueOption[] valueOptions?: WorkflowVariableValueOption[]
} }
export type WorkflowVariableOption = {
value: string
label: string
subtitle: string
description: string
}
export type WorkflowNodeSpec = AIWorkflowNodeSpec export type WorkflowNodeSpec = AIWorkflowNodeSpec
export type WorkflowDraftValidation = { export type WorkflowDraftValidation = {
@@ -92,6 +99,17 @@ export function refField(value: WorkflowValue | undefined): string {
return isRefValue(value) ? value.content[1] : "" return isRefValue(value) ? value.content[1] : ""
} }
export function buildVariableOption(variable: WorkflowVariableRef): WorkflowVariableOption {
const ref = `${variable.nodeId}.${variable.field}`
const label = `${variable.nodeName} / ${variable.label || variable.field}`
return {
value: ref,
label,
subtitle: [ref, variable.type].filter(Boolean).join(" · "),
description: variable.description,
}
}
export function getNodeTitle( export function getNodeTitle(
node: AIWorkflowDefinition["nodes"][number] | undefined, node: AIWorkflowDefinition["nodes"][number] | undefined,
specs: AIWorkflowNodeSpec[] = [] specs: AIWorkflowNodeSpec[] = []
+7 -1
View File
@@ -23,6 +23,7 @@ import { useI18n } from "@/i18n/provider"
export type ComboboxOption = { export type ComboboxOption = {
value: string value: string
label: string label: string
subtitle?: string
description?: string description?: string
} }
@@ -78,7 +79,7 @@ export function OptionCombobox({
{options.map((option) => ( {options.map((option) => (
<CommandItem <CommandItem
key={option.value} key={option.value}
value={`${option.label} ${option.value} ${option.description ?? ""}`} value={`${option.label} ${option.value} ${option.subtitle ?? ""} ${option.description ?? ""}`}
onSelect={() => { onSelect={() => {
onChange(option.value) onChange(option.value)
setOpen(false) setOpen(false)
@@ -94,6 +95,11 @@ export function OptionCombobox({
/> />
<span className="min-w-0"> <span className="min-w-0">
<span className="block truncate">{option.label}</span> <span className="block truncate">{option.label}</span>
{option.subtitle ? (
<span className="mt-0.5 block truncate font-mono text-[11px] leading-4 text-slate-500">
{option.subtitle}
</span>
) : null}
{option.description ? ( {option.description ? (
<span className="mt-0.5 line-clamp-2 text-xs leading-4 text-muted-foreground"> <span className="mt-0.5 line-clamp-2 text-xs leading-4 text-muted-foreground">
{option.description} {option.description}