refactor(workflow): simplify edge configuration
This commit is contained in:
@@ -785,22 +785,9 @@ class WorkflowBrain(BaseBrain):
|
|||||||
triggering_user_message: dict[str, Any] | None = None,
|
triggering_user_message: dict[str, Any] | None = None,
|
||||||
) -> NodeConfig:
|
) -> NodeConfig:
|
||||||
await self._begin_edge_transition(edge)
|
await self._begin_edge_transition(edge)
|
||||||
context_messages = list(leading_messages or [])
|
|
||||||
speech = self._engine.edge_transition_speech(edge)
|
|
||||||
if speech:
|
|
||||||
content = self._store.render(speech).strip()
|
|
||||||
if content:
|
|
||||||
await self._queue_visible_speech(
|
|
||||||
content,
|
|
||||||
source="workflow-edge-transition",
|
|
||||||
node_id=str(edge.get("target") or "") or None,
|
|
||||||
)
|
|
||||||
context_message = fixed_speech_context_message(content)
|
|
||||||
if context_message is not None:
|
|
||||||
context_messages.append(context_message)
|
|
||||||
return await self._resolve_path(
|
return await self._resolve_path(
|
||||||
str(edge.get("target") or ""),
|
str(edge.get("target") or ""),
|
||||||
leading_messages=context_messages,
|
leading_messages=list(leading_messages or []),
|
||||||
triggering_user_text=triggering_user_text,
|
triggering_user_text=triggering_user_text,
|
||||||
triggering_user_message=triggering_user_message,
|
triggering_user_message=triggering_user_message,
|
||||||
)
|
)
|
||||||
@@ -863,19 +850,6 @@ class WorkflowBrain(BaseBrain):
|
|||||||
if not edge:
|
if not edge:
|
||||||
return self._passive_node_config(node_id, context_messages)
|
return self._passive_node_config(node_id, context_messages)
|
||||||
await self._begin_edge_transition(edge)
|
await self._begin_edge_transition(edge)
|
||||||
speech = self._engine.edge_transition_speech(edge)
|
|
||||||
if speech:
|
|
||||||
content = self._store.render(speech).strip()
|
|
||||||
if content:
|
|
||||||
target_id = str(edge.get("target") or "")
|
|
||||||
await self._queue_visible_speech(
|
|
||||||
content,
|
|
||||||
source="workflow-edge-transition",
|
|
||||||
node_id=target_id or None,
|
|
||||||
)
|
|
||||||
context_message = fixed_speech_context_message(content)
|
|
||||||
if context_message is not None:
|
|
||||||
context_messages.append(context_message)
|
|
||||||
node_id = str(edge.get("target") or "")
|
node_id = str(edge.get("target") or "")
|
||||||
raise RuntimeError("工作流连续自动跳转超过安全上限")
|
raise RuntimeError("工作流连续自动跳转超过安全上限")
|
||||||
|
|
||||||
|
|||||||
@@ -138,17 +138,17 @@ def node_types_response() -> dict[str, Any]:
|
|||||||
|
|
||||||
def _edge_data_v3(edge: dict) -> dict:
|
def _edge_data_v3(edge: dict) -> dict:
|
||||||
data = deepcopy(edge.get("data") or {})
|
data = deepcopy(edge.get("data") or {})
|
||||||
|
data.pop("transitionSpeech", None)
|
||||||
|
data.pop("transition_speech", None)
|
||||||
if data.get("mode") in EDGE_MODES:
|
if data.get("mode") in EDGE_MODES:
|
||||||
data.setdefault("priority", 10)
|
data.setdefault("priority", 10)
|
||||||
return data
|
return data
|
||||||
condition = str(data.pop("condition", "") or "").strip()
|
condition = str(data.pop("condition", "") or "").strip()
|
||||||
transition = data.pop("transition_speech", data.get("transitionSpeech", ""))
|
|
||||||
data.update(
|
data.update(
|
||||||
{
|
{
|
||||||
"mode": "llm" if condition else "always",
|
"mode": "llm" if condition else "always",
|
||||||
"priority": 10,
|
"priority": 10,
|
||||||
"condition": condition,
|
"condition": condition,
|
||||||
"transitionSpeech": transition,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return data
|
return data
|
||||||
@@ -234,6 +234,8 @@ def normalize_graph(graph: dict[str, Any] | None) -> dict[str, Any]:
|
|||||||
_normalize_settings(settings)
|
_normalize_settings(settings)
|
||||||
source.setdefault("nodes", [])
|
source.setdefault("nodes", [])
|
||||||
source.setdefault("edges", [])
|
source.setdefault("edges", [])
|
||||||
|
for edge in source["edges"]:
|
||||||
|
edge["data"] = _edge_data_v3(edge)
|
||||||
for node in source["nodes"]:
|
for node in source["nodes"]:
|
||||||
data = node.setdefault("data", {})
|
data = node.setdefault("data", {})
|
||||||
if node.get("type") == "start":
|
if node.get("type") == "start":
|
||||||
@@ -327,7 +329,7 @@ def normalize_graph(graph: dict[str, Any] | None) -> dict[str, Any]:
|
|||||||
"id": f"e-{start_id}-{synthetic_id}",
|
"id": f"e-{start_id}-{synthetic_id}",
|
||||||
"source": start_id,
|
"source": start_id,
|
||||||
"target": synthetic_id,
|
"target": synthetic_id,
|
||||||
"data": {"mode": "always", "priority": 0, "transitionSpeech": ""},
|
"data": {"mode": "always", "priority": 0},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -113,14 +113,6 @@ class WorkflowEngine:
|
|||||||
return f"当满足以下条件时转到「{target}」:{condition}"
|
return f"当满足以下条件时转到「{target}」:{condition}"
|
||||||
return f"当当前阶段任务完成时转到「{target}」。"
|
return f"当当前阶段任务完成时转到「{target}」。"
|
||||||
|
|
||||||
def edge_transition_speech(self, edge: dict | None) -> str:
|
|
||||||
if not edge:
|
|
||||||
return ""
|
|
||||||
data = edge.get("data") or {}
|
|
||||||
return str(
|
|
||||||
data.get("transitionSpeech") or data.get("transition_speech") or ""
|
|
||||||
)
|
|
||||||
|
|
||||||
def global_prompt(self) -> str:
|
def global_prompt(self) -> str:
|
||||||
return str(self.settings.get("globalPrompt") or "").strip()
|
return str(self.settings.get("globalPrompt") or "").strip()
|
||||||
|
|
||||||
|
|||||||
@@ -2393,7 +2393,6 @@ class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
"mode": "llm",
|
"mode": "llm",
|
||||||
"priority": 10,
|
"priority": 10,
|
||||||
"condition": "需求已收集",
|
"condition": "需求已收集",
|
||||||
"transitionSpeech": "正在为你结束流程",
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -2569,29 +2568,6 @@ class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
self.assertTrue(call_end.ending)
|
self.assertTrue(call_end.ending)
|
||||||
self.assertTrue(call_end.armed)
|
self.assertTrue(call_end.armed)
|
||||||
self.assertTrue(any(getattr(frame, "text", "") == "感谢来电" for frame in queued))
|
self.assertTrue(any(getattr(frame, "text", "") == "感谢来电" for frame in queued))
|
||||||
transition_context_frames = [
|
|
||||||
frame
|
|
||||||
for frame in worker.frames
|
|
||||||
if isinstance(frame, LLMMessagesAppendFrame)
|
|
||||||
and frame.messages
|
|
||||||
== [
|
|
||||||
{
|
|
||||||
"role": "system",
|
|
||||||
"content": (
|
|
||||||
f"{FIXED_SPEECH_CONTEXT_MARKER}\n正在为你结束流程"
|
|
||||||
),
|
|
||||||
}
|
|
||||||
]
|
|
||||||
]
|
|
||||||
self.assertTrue(transition_context_frames)
|
|
||||||
transition_events = [
|
|
||||||
frame.message
|
|
||||||
for frame in queued
|
|
||||||
if isinstance(frame, OutputTransportMessageUrgentFrame)
|
|
||||||
and frame.message.get("source") == "workflow-edge-transition"
|
|
||||||
]
|
|
||||||
self.assertEqual(transition_events[0]["content"], "正在为你结束流程")
|
|
||||||
self.assertEqual(transition_events[0]["nodeId"], "end")
|
|
||||||
assistant_transcripts = [
|
assistant_transcripts = [
|
||||||
frame.message.get("content")
|
frame.message.get("content")
|
||||||
for frame in queued
|
for frame in queued
|
||||||
@@ -2601,11 +2577,7 @@ class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
]
|
]
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
assistant_transcripts,
|
assistant_transcripts,
|
||||||
["正在为你结束流程", "感谢来电"],
|
["感谢来电"],
|
||||||
)
|
|
||||||
self.assertIn(
|
|
||||||
"正在为你结束流程",
|
|
||||||
brain._store.values["system__conversation_history"],
|
|
||||||
)
|
)
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
"感谢来电",
|
"感谢来电",
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ class _Brain:
|
|||||||
|
|
||||||
async def on_client_ready(self):
|
async def on_client_ready(self):
|
||||||
for content, timestamp in (
|
for content, timestamp in (
|
||||||
("Start Edge 过渡语", "2026-07-14T10:00:00.200+00:00"),
|
("Message 节点播报", "2026-07-14T10:00:00.200+00:00"),
|
||||||
("Agent 固定进入语", "2026-07-14T10:00:00.300+00:00"),
|
("Agent 固定进入语", "2026-07-14T10:00:00.300+00:00"),
|
||||||
):
|
):
|
||||||
await self.worker.queue_frame(
|
await self.worker.queue_frame(
|
||||||
@@ -118,7 +118,7 @@ class PipelineEventTest(unittest.IsolatedAsyncioTestCase):
|
|||||||
ordered = sorted(transcripts, key=lambda message: message["timestamp"])
|
ordered = sorted(transcripts, key=lambda message: message["timestamp"])
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
[message["content"] for message in ordered],
|
[message["content"] for message in ordered],
|
||||||
["助手开场白", "Start Edge 过渡语", "Agent 固定进入语"],
|
["助手开场白", "Message 节点播报", "Agent 固定进入语"],
|
||||||
)
|
)
|
||||||
self.assertEqual(transcripts[0]["timestamp"], greeting_time)
|
self.assertEqual(transcripts[0]["timestamp"], greeting_time)
|
||||||
self.assertEqual(brain.prepared_greeting, "助手开场白")
|
self.assertEqual(brain.prepared_greeting, "助手开场白")
|
||||||
|
|||||||
@@ -132,6 +132,17 @@ class WorkflowGraphTests(unittest.TestCase):
|
|||||||
self.assertEqual(cleaned_agent["data"]["entryMode"], "wait_user")
|
self.assertEqual(cleaned_agent["data"]["entryMode"], "wait_user")
|
||||||
self.assertNotIn("entrySpeech", cleaned_agent["data"])
|
self.assertNotIn("entrySpeech", cleaned_agent["data"])
|
||||||
|
|
||||||
|
def test_edge_speech_fields_are_removed(self):
|
||||||
|
graph = valid_graph()
|
||||||
|
graph["edges"][0]["data"]["transitionSpeech"] = "不再播放"
|
||||||
|
graph["edges"][1]["data"]["transition_speech"] = "旧字段也不再播放"
|
||||||
|
|
||||||
|
normalized = normalize_graph(graph)
|
||||||
|
|
||||||
|
for edge in normalized["edges"]:
|
||||||
|
self.assertNotIn("transitionSpeech", edge["data"])
|
||||||
|
self.assertNotIn("transition_speech", edge["data"])
|
||||||
|
|
||||||
def test_action_defaults_preserve_legacy_result_assignment_behavior(self):
|
def test_action_defaults_preserve_legacy_result_assignment_behavior(self):
|
||||||
graph = valid_graph()
|
graph = valid_graph()
|
||||||
graph["nodes"].extend(
|
graph["nodes"].extend(
|
||||||
|
|||||||
@@ -93,12 +93,16 @@ export function EditableTitle({
|
|||||||
placeholder = "未命名助手",
|
placeholder = "未命名助手",
|
||||||
editLabel = "助手名称",
|
editLabel = "助手名称",
|
||||||
variant = "page",
|
variant = "page",
|
||||||
|
allowEmpty = false,
|
||||||
|
maxLength,
|
||||||
}: {
|
}: {
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
editLabel?: string;
|
editLabel?: string;
|
||||||
variant?: "page" | "panel";
|
variant?: "page" | "panel";
|
||||||
|
allowEmpty?: boolean;
|
||||||
|
maxLength?: number;
|
||||||
}) {
|
}) {
|
||||||
const [editing, setEditing] = useState(false);
|
const [editing, setEditing] = useState(false);
|
||||||
const [draft, setDraft] = useState(value);
|
const [draft, setDraft] = useState(value);
|
||||||
@@ -118,7 +122,7 @@ export function EditableTitle({
|
|||||||
|
|
||||||
function commit() {
|
function commit() {
|
||||||
const next = draft.trim();
|
const next = draft.trim();
|
||||||
if (next) {
|
if (next || allowEmpty) {
|
||||||
onChange(next);
|
onChange(next);
|
||||||
}
|
}
|
||||||
setEditing(false);
|
setEditing(false);
|
||||||
@@ -129,6 +133,7 @@ export function EditableTitle({
|
|||||||
<input
|
<input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
value={draft}
|
value={draft}
|
||||||
|
maxLength={maxLength}
|
||||||
onChange={(event) => setDraft(event.target.value)}
|
onChange={(event) => setDraft(event.target.value)}
|
||||||
onBlur={commit}
|
onBlur={commit}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ import {
|
|||||||
type NodeSpecMap,
|
type NodeSpecMap,
|
||||||
type RuntimeNodeSpec,
|
type RuntimeNodeSpec,
|
||||||
type WorkflowGraph,
|
type WorkflowGraph,
|
||||||
|
type WorkflowEdgeData,
|
||||||
type WorkflowNodeData,
|
type WorkflowNodeData,
|
||||||
type WorkflowNodeType,
|
type WorkflowNodeType,
|
||||||
} from "./specs";
|
} from "./specs";
|
||||||
@@ -396,7 +397,7 @@ export function WorkflowCanvas({
|
|||||||
const updateEdgeData = useCallback(
|
const updateEdgeData = useCallback(
|
||||||
(
|
(
|
||||||
id: string,
|
id: string,
|
||||||
patch: WorkflowGraph["edges"][number]["data"],
|
patch: Partial<WorkflowEdgeData>,
|
||||||
) => {
|
) => {
|
||||||
setEdges((es) =>
|
setEdges((es) =>
|
||||||
es.map((e) => {
|
es.map((e) => {
|
||||||
@@ -535,6 +536,7 @@ export function WorkflowCanvas({
|
|||||||
const editingNode = nodes.find((n) => n.id === editingNodeId);
|
const editingNode = nodes.find((n) => n.id === editingNodeId);
|
||||||
const editingSpec = editingNode ? specsByType[editingNode.type as string] : null;
|
const editingSpec = editingNode ? specsByType[editingNode.type as string] : null;
|
||||||
const editingEdge = edges.find((e) => e.id === editingEdgeId);
|
const editingEdge = edges.find((e) => e.id === editingEdgeId);
|
||||||
|
const editingEdgeData = editingEdge?.data as WorkflowEdgeData | undefined;
|
||||||
const addableSpecs = Object.values(specsByType).filter((s) => s.addable);
|
const addableSpecs = Object.values(specsByType).filter((s) => s.addable);
|
||||||
const canAddSpec = useCallback(
|
const canAddSpec = useCallback(
|
||||||
(spec: RuntimeNodeSpec) => {
|
(spec: RuntimeNodeSpec) => {
|
||||||
@@ -683,7 +685,27 @@ export function WorkflowCanvas({
|
|||||||
: settingsOpen
|
: settingsOpen
|
||||||
? "工作流设置"
|
? "工作流设置"
|
||||||
: editingEdge
|
: editingEdge
|
||||||
? "编辑连接条件"
|
? (
|
||||||
|
<EditableTitle
|
||||||
|
value={editingEdgeData?.label ?? ""}
|
||||||
|
placeholder={
|
||||||
|
editingEdgeData?.mode === "llm"
|
||||||
|
? "大模型判断"
|
||||||
|
: editingEdgeData?.mode === "expression"
|
||||||
|
? "表达式"
|
||||||
|
: "默认路径"
|
||||||
|
}
|
||||||
|
editLabel="连接名称"
|
||||||
|
variant="panel"
|
||||||
|
allowEmpty
|
||||||
|
maxLength={64}
|
||||||
|
onChange={(label) =>
|
||||||
|
updateEdgeData(editingEdge.id, {
|
||||||
|
label: label || undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
: editingNode && editingSpec
|
: editingNode && editingSpec
|
||||||
? (
|
? (
|
||||||
<EditableTitle
|
<EditableTitle
|
||||||
|
|||||||
@@ -3,10 +3,7 @@
|
|||||||
import type { Edge } from "@xyflow/react";
|
import type { Edge } from "@xyflow/react";
|
||||||
import {
|
import {
|
||||||
Braces,
|
Braces,
|
||||||
CircleCheck,
|
|
||||||
CircleX,
|
|
||||||
GitBranch,
|
GitBranch,
|
||||||
MessageSquareText,
|
|
||||||
Plus,
|
Plus,
|
||||||
Trash2,
|
Trash2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
@@ -38,14 +35,11 @@ export function EdgeSettingsPanel({
|
|||||||
sourceType?: string;
|
sourceType?: string;
|
||||||
isOnlyOutgoing: boolean;
|
isOnlyOutgoing: boolean;
|
||||||
hasOtherDefaultPath: boolean;
|
hasOtherDefaultPath: boolean;
|
||||||
onChange: (patch: WorkflowEdgeData) => void;
|
onChange: (patch: Partial<WorkflowEdgeData>) => void;
|
||||||
}) {
|
}) {
|
||||||
const data = (edge.data ?? { mode: "always", priority: 10 }) as WorkflowEdgeData;
|
const data = (edge.data ?? { mode: "always", priority: 10 }) as WorkflowEdgeData;
|
||||||
const [mode, setMode] = useState(data.mode ?? "always");
|
const [mode, setMode] = useState(data.mode ?? "always");
|
||||||
const [priority, setPriority] = useState(data.priority ?? 10);
|
|
||||||
const [label, setLabel] = useState(data.label ?? "");
|
|
||||||
const [condition, setCondition] = useState(data.condition ?? "");
|
const [condition, setCondition] = useState(data.condition ?? "");
|
||||||
const [transitionSpeech, setTransitionSpeech] = useState(data.transitionSpeech ?? "");
|
|
||||||
const [combinator, setCombinator] = useState<"and" | "or">(
|
const [combinator, setCombinator] = useState<"and" | "or">(
|
||||||
data.expression?.combinator ?? "and",
|
data.expression?.combinator ?? "and",
|
||||||
);
|
);
|
||||||
@@ -57,33 +51,22 @@ export function EdgeSettingsPanel({
|
|||||||
|
|
||||||
const publish = ({
|
const publish = ({
|
||||||
nextMode = mode,
|
nextMode = mode,
|
||||||
nextPriority = priority,
|
|
||||||
nextLabel = label,
|
|
||||||
nextCondition = condition,
|
nextCondition = condition,
|
||||||
nextTransitionSpeech = transitionSpeech,
|
|
||||||
nextCombinator = combinator,
|
nextCombinator = combinator,
|
||||||
nextRules = rules,
|
nextRules = rules,
|
||||||
}: {
|
}: {
|
||||||
nextMode?: WorkflowEdgeData["mode"];
|
nextMode?: WorkflowEdgeData["mode"];
|
||||||
nextPriority?: number;
|
|
||||||
nextLabel?: string;
|
|
||||||
nextCondition?: string;
|
nextCondition?: string;
|
||||||
nextTransitionSpeech?: string;
|
|
||||||
nextCombinator?: "and" | "or";
|
nextCombinator?: "and" | "or";
|
||||||
nextRules?: ExpressionRule[];
|
nextRules?: ExpressionRule[];
|
||||||
}) =>
|
}) =>
|
||||||
onChange({
|
onChange({
|
||||||
mode: nextMode,
|
mode: nextMode,
|
||||||
priority: nextPriority,
|
|
||||||
label: nextLabel.trim() ? nextLabel : undefined,
|
|
||||||
condition: nextMode === "llm" ? nextCondition : undefined,
|
condition: nextMode === "llm" ? nextCondition : undefined,
|
||||||
expression:
|
expression:
|
||||||
nextMode === "expression"
|
nextMode === "expression"
|
||||||
? { combinator: nextCombinator, rules: nextRules }
|
? { combinator: nextCombinator, rules: nextRules }
|
||||||
: undefined,
|
: undefined,
|
||||||
transitionSpeech: nextTransitionSpeech.trim()
|
|
||||||
? nextTransitionSpeech
|
|
||||||
: undefined,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const setRule = (index: number, patch: Partial<ExpressionRule>) => {
|
const setRule = (index: number, patch: Partial<ExpressionRule>) => {
|
||||||
@@ -94,24 +77,6 @@ export function EdgeSettingsPanel({
|
|||||||
publish({ nextRules });
|
publish({ nextRules });
|
||||||
};
|
};
|
||||||
|
|
||||||
const applyActionResultPreset = (status: "ok" | "error") => {
|
|
||||||
const nextRules: ExpressionRule[] = [
|
|
||||||
{
|
|
||||||
variable: "system__last_action_status",
|
|
||||||
operator: "eq",
|
|
||||||
value: status,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
setMode("expression");
|
|
||||||
setCombinator("and");
|
|
||||||
setRules(nextRules);
|
|
||||||
publish({
|
|
||||||
nextMode: "expression",
|
|
||||||
nextCombinator: "and",
|
|
||||||
nextRules,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const parseValue = (value: string): unknown => {
|
const parseValue = (value: string): unknown => {
|
||||||
if (value === "true") return true;
|
if (value === "true") return true;
|
||||||
if (value === "false") return false;
|
if (value === "false") return false;
|
||||||
@@ -159,82 +124,13 @@ export function EdgeSettingsPanel({
|
|||||||
Agent 不能只有默认路径;请改为条件路径,或删除连接以持续对话。
|
Agent 不能只有默认路径;请改为条件路径,或删除连接以持续对话。
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{sourceType === "action" && (
|
|
||||||
<div className="rounded-xl border border-hairline bg-canvas-soft p-3">
|
|
||||||
<div className="mb-2 text-xs text-muted-foreground">
|
|
||||||
Action 常用结果条件
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
className="gap-1.5"
|
|
||||||
onClick={() => applyActionResultPreset("ok")}
|
|
||||||
>
|
|
||||||
<CircleCheck size={14} />
|
|
||||||
执行成功
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
className="gap-1.5"
|
|
||||||
onClick={() => applyActionResultPreset("error")}
|
|
||||||
>
|
|
||||||
<CircleX size={14} />
|
|
||||||
执行失败
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{mode !== "always" && (
|
|
||||||
<label className="block">
|
|
||||||
<div className="mb-1.5 text-sm font-medium text-foreground">
|
|
||||||
优先级
|
|
||||||
</div>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
value={priority}
|
|
||||||
onChange={(event) => {
|
|
||||||
const nextPriority = Number(event.target.value) || 0;
|
|
||||||
setPriority(nextPriority);
|
|
||||||
publish({ nextPriority });
|
|
||||||
}}
|
|
||||||
className="border-hairline-strong bg-background text-foreground"
|
|
||||||
/>
|
|
||||||
<span className="mt-1.5 block text-xs text-muted-foreground">
|
|
||||||
条件路径按数字从小到大判断;默认路径不参与优先级。
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
</SectionCard>
|
</SectionCard>
|
||||||
|
|
||||||
<SectionCard
|
<SectionCard
|
||||||
icon={<Braces size={15} />}
|
icon={<Braces size={15} />}
|
||||||
title="触发条件"
|
title="触发条件"
|
||||||
description="配置画布标签以及这条连接被命中的条件"
|
description="配置这条连接被命中的条件"
|
||||||
>
|
>
|
||||||
<label className="block">
|
|
||||||
<div className="mb-1.5 text-sm font-medium text-foreground">
|
|
||||||
条件标签
|
|
||||||
</div>
|
|
||||||
<Input
|
|
||||||
value={label}
|
|
||||||
maxLength={64}
|
|
||||||
placeholder="例如:用户想转人工"
|
|
||||||
onChange={(event) => {
|
|
||||||
const nextLabel = event.target.value;
|
|
||||||
setLabel(nextLabel);
|
|
||||||
publish({ nextLabel });
|
|
||||||
}}
|
|
||||||
className="border-hairline-strong bg-background text-foreground placeholder:text-muted-soft"
|
|
||||||
/>
|
|
||||||
<span className="mt-1.5 block text-xs text-muted-foreground">
|
|
||||||
用于画布和日志中识别该路径,{label.length}/64
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
{mode === "llm" && (
|
{mode === "llm" && (
|
||||||
<label className="block">
|
<label className="block">
|
||||||
<div className="mb-1.5 text-sm font-medium text-foreground">
|
<div className="mb-1.5 text-sm font-medium text-foreground">
|
||||||
@@ -376,32 +272,6 @@ export function EdgeSettingsPanel({
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</SectionCard>
|
</SectionCard>
|
||||||
|
|
||||||
<SectionCard
|
|
||||||
icon={<MessageSquareText size={15} />}
|
|
||||||
title="过渡语"
|
|
||||||
description="命中连接后、进入下一节点前播放的固定内容"
|
|
||||||
>
|
|
||||||
<label className="block">
|
|
||||||
<div className="mb-1.5 text-sm font-medium text-foreground">
|
|
||||||
固定过渡语(可选)
|
|
||||||
</div>
|
|
||||||
<Textarea
|
|
||||||
rows={3}
|
|
||||||
value={transitionSpeech}
|
|
||||||
placeholder="例如:好的,正在为你转接。"
|
|
||||||
onChange={(event) => {
|
|
||||||
const nextTransitionSpeech = event.target.value;
|
|
||||||
setTransitionSpeech(nextTransitionSpeech);
|
|
||||||
publish({ nextTransitionSpeech });
|
|
||||||
}}
|
|
||||||
className="field-sizing-fixed min-h-24 resize-y border-hairline-strong bg-background text-sm text-foreground placeholder:text-muted-soft"
|
|
||||||
/>
|
|
||||||
<span className="mt-1.5 block text-xs text-muted-foreground">
|
|
||||||
会显示在调试与完整聊天记录中,同时使用当前 TTS 播放。
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
</SectionCard>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,11 +121,11 @@ export type ExpressionRule = {
|
|||||||
|
|
||||||
export type WorkflowEdgeData = {
|
export type WorkflowEdgeData = {
|
||||||
mode: EdgeMode;
|
mode: EdgeMode;
|
||||||
|
/** Internal route order, assigned automatically when an edge is created. */
|
||||||
priority: number;
|
priority: number;
|
||||||
condition?: string;
|
condition?: string;
|
||||||
expression?: { combinator: "and" | "or"; rules: ExpressionRule[] };
|
expression?: { combinator: "and" | "or"; rules: ExpressionRule[] };
|
||||||
label?: string;
|
label?: string;
|
||||||
transitionSpeech?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FieldSpec = {
|
export type FieldSpec = {
|
||||||
|
|||||||
Reference in New Issue
Block a user