|
|
@@ -32,7 +32,7 @@ import ConditionNode from '../../components/workflow/nodes/ConditionNode.vue'
|
|
|
import SmartActionNode from '../../components/workflow/nodes/SmartActionNode.vue'
|
|
|
import VirtualGroupNode from '../../components/workflow/nodes/VirtualGroupNode.vue'
|
|
|
import CodeEditor from '../../components/skill/CodeEditor.vue'
|
|
|
-import { inferNewEdge, refreshMappingsForNode, getEdgeStyle, getNodeOutputs, IO_TYPES } from '../../utils/ioInference'
|
|
|
+import { inferNewEdge, refreshMappingsForNode, getNodeOutputs, getReachablePredecessors, IO_TYPES } from '../../utils/ioInference'
|
|
|
import { useVersionHistory } from '../../composables/useVersionHistory'
|
|
|
import { useWorkflowRunner } from '../../composables/useWorkflowRunner'
|
|
|
import { useNodeFields } from '../../composables/useNodeFields'
|
|
|
@@ -175,7 +175,8 @@ onConnect((params) => {
|
|
|
...params,
|
|
|
type: 'bezier',
|
|
|
animated: true,
|
|
|
- style: inferred?.style || defaultEdgeStyle,
|
|
|
+ // 视觉上统一灰色,io 推断状态仅写入 data 供属性面板展示(不再影响线条颜色)
|
|
|
+ style: defaultEdgeStyle,
|
|
|
data: inferred?.data || {}
|
|
|
}])
|
|
|
})
|
|
|
@@ -227,13 +228,12 @@ onNodeDragStop(() => {
|
|
|
virtualEdgeDragInProgress = false
|
|
|
})
|
|
|
|
|
|
-// 批量更新边的映射数据和样式
|
|
|
+// 批量更新边的映射数据(仅写入 data,不再改变 style;视觉统一灰色,避免不同映射状态造成颜色差异)
|
|
|
function applyEdgeMappingUpdates(updates) {
|
|
|
for (const upd of updates) {
|
|
|
const edge = getEdges.value.find(e => e.id === upd.id)
|
|
|
if (edge) {
|
|
|
edge.data = { ...edge.data, ...upd.data }
|
|
|
- edge.style = upd.style
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -263,12 +263,13 @@ const nodeTypeInfo = {
|
|
|
|
|
|
function getDefaultData(type) {
|
|
|
switch (type) {
|
|
|
- case 'userInput': return { label: '用户输入', variables: [] }
|
|
|
- case 'llm': return { label: '大模型处理', modelId: null, systemPrompt: '', userPrompt: '', inputs: [], outputs: [{ name: 'result', label: 'LLM 输出', type: 'string', description: '' }], failStrategy: 'abort' }
|
|
|
- case 'agent': return { label: '智能体', agentId: '', agentName: '', modelId: null, failStrategy: 'abort' }
|
|
|
- case 'skill': return { label: '技能', skillId: '', skillName: '', modelId: null, failStrategy: 'abort' }
|
|
|
- case 'smartAction': return { label: '智能操作', actionPrompt: '', modelId: null, inputs: [], outputs: [{ name: 'result', label: '操作结果', type: 'string', description: '' }], failStrategy: 'abort' }
|
|
|
+ case 'userInput': return { nodeName: '', label: '用户输入', variables: [] }
|
|
|
+ case 'llm': return { nodeName: '', label: '大模型处理', modelId: null, systemPrompt: '', userPrompt: '', inputs: [], outputs: [{ name: 'result', label: 'LLM 输出', type: 'string', description: '' }], failStrategy: 'abort' }
|
|
|
+ case 'agent': return { nodeName: '', label: '智能体', agentId: '', agentName: '', modelId: null, failStrategy: 'abort' }
|
|
|
+ case 'skill': return { nodeName: '', label: '技能', skillId: '', skillName: '', modelId: null, failStrategy: 'abort' }
|
|
|
+ case 'smartAction': return { nodeName: '', label: '智能操作', actionPrompt: '', modelId: null, inputs: [], outputs: [{ name: 'result', label: '操作结果', type: 'string', description: '' }], failStrategy: 'abort' }
|
|
|
case 'knowledgeRetrieval': return {
|
|
|
+ nodeName: '',
|
|
|
label: '知识库检索',
|
|
|
source: 'document',
|
|
|
query: '',
|
|
|
@@ -281,9 +282,9 @@ function getDefaultData(type) {
|
|
|
knowledgeBaseId: null,
|
|
|
topK: 5
|
|
|
}
|
|
|
- case 'condition': return { label: '条件分支', conditions: [{ type: 'IF', expression: '' }] }
|
|
|
- case 'output': return { label: '输出', fields: [{ name: 'result', label: '输出结果', type: 'string', description: '' }] }
|
|
|
- default: return { label: '节点' }
|
|
|
+ case 'condition': return { nodeName: '', label: '条件分支', conditions: [{ type: 'IF', expression: '' }] }
|
|
|
+ case 'output': return { nodeName: '', label: '输出', fields: [{ name: 'result', label: '输出结果', type: 'string', description: '' }] }
|
|
|
+ default: return { nodeName: '', label: '节点' }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -402,19 +403,18 @@ const selectedEdge = computed(() => {
|
|
|
return getEdges.value.find(e => e.id === selectedEdgeId.value) || null
|
|
|
})
|
|
|
|
|
|
-// ========== 输入变量关联:列出所有直接上游节点的输出字段供选择 ==========
|
|
|
+// ========== 输入变量关联:列出所有可达前驱节点的输出字段供选择 ==========
|
|
|
+// 关联范围:不仅限直接上游,而是通过边反向 BFS 找出画布中所有可达的前序节点
|
|
|
const inputMappingOptions = computed(() => {
|
|
|
if (!selectedNode.value) return []
|
|
|
+ const predecessors = getReachablePredecessors(selectedNode.value.id, getNodes.value, getEdges.value)
|
|
|
const opts = []
|
|
|
- for (const edge of getEdges.value) {
|
|
|
- if (edge.target !== selectedNode.value.id) continue
|
|
|
- const src = getNodes.value.find(n => n.id === edge.source)
|
|
|
- if (!src) continue
|
|
|
+ for (const src of predecessors) {
|
|
|
const outputs = getNodeOutputs(src)
|
|
|
for (const out of outputs) {
|
|
|
const value = JSON.stringify({ nodeId: src.id, field: out.name })
|
|
|
opts.push({
|
|
|
- label: `${src.data?.label || src.id}.${out.name}`,
|
|
|
+ label: `${src.data?.nodeName || src.data?.label || src.id}.${out.name}`,
|
|
|
value,
|
|
|
nodeId: src.id,
|
|
|
field: out.name
|
|
|
@@ -444,6 +444,22 @@ function clearInputMapping(input) {
|
|
|
onFieldChange('inputs', [...selectedData.value.inputs])
|
|
|
}
|
|
|
|
|
|
+// ========== 节点名称编辑(含重名校验) ==========
|
|
|
+function updateNodeName(value) {
|
|
|
+ const trimmed = (value || '').trim()
|
|
|
+ if (trimmed && selectedNode.value) {
|
|
|
+ const duplicate = getNodes.value.find(n =>
|
|
|
+ n.id !== selectedNode.value.id &&
|
|
|
+ n.data?.nodeName === trimmed
|
|
|
+ )
|
|
|
+ if (duplicate) {
|
|
|
+ message.warning(`节点名称「${trimmed}」已被其他节点使用`)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ }
|
|
|
+ onFieldChange('nodeName', trimmed)
|
|
|
+}
|
|
|
+
|
|
|
// ========== 智能体选项 ==========
|
|
|
const agentOptions = computed(() =>
|
|
|
(skillStore.skills || []).map(s => ({
|
|
|
@@ -937,7 +953,8 @@ async function applyVersionToCanvas(wf) {
|
|
|
...e,
|
|
|
type: 'bezier',
|
|
|
animated: true,
|
|
|
- style: getEdgeStyle(e.data?.status),
|
|
|
+ style: defaultEdgeStyle,
|
|
|
+ data: e.data || {}
|
|
|
})))
|
|
|
await nextTick()
|
|
|
for (const node of getNodes.value) {
|
|
|
@@ -1042,7 +1059,7 @@ function applyGraphData(graphData) {
|
|
|
</n-icon>
|
|
|
</button>
|
|
|
<div class="toolbar-form">
|
|
|
- <div class="wf-field">
|
|
|
+ <div class="wf-field wf-field-show">
|
|
|
<label class="wf-label">
|
|
|
智能体展示名称
|
|
|
<span class="label-hint">(可包含中文)</span>
|
|
|
@@ -1050,34 +1067,34 @@ function applyGraphData(graphData) {
|
|
|
<n-input v-model:value="form.displayName" placeholder="如:军事分析助手" style="width: 200px" size="small"
|
|
|
:maxlength="100" />
|
|
|
</div>
|
|
|
- <div class="wf-field">
|
|
|
+ <div class="wf-field wf-field-name">
|
|
|
<label class="wf-label">
|
|
|
智能体名称
|
|
|
<span class="label-hint">(kebab-case,唯一标识,禁用大写/空格/下划线/中文)</span>
|
|
|
</label>
|
|
|
- <n-input v-model:value="form.name" placeholder="如:military-analyze-agent" style="width: 220px" size="small"
|
|
|
+ <n-input v-model:value="form.name" placeholder="如:military-analyze-agent" size="small"
|
|
|
:maxlength="64"
|
|
|
:status="nameState.status === 'invalid' ? 'error' : nameState.status === 'will-rename' || nameState.status === 'will-normalize' ? 'warning' : 'success'" />
|
|
|
- <div class="wf-name-hint">
|
|
|
- <span v-if="nameState.status === 'will-rename'" class="hint-warn">
|
|
|
- ⚠ 保存时将重命名:<code>{{ routeName }}</code> → <code>{{ nameState.normalized }}</code>
|
|
|
- </span>
|
|
|
- <span v-else-if="nameState.status === 'will-normalize'" class="hint-warn">
|
|
|
- 将规范化为:<code>{{ nameState.normalized }}</code>
|
|
|
- </span>
|
|
|
- <span v-else-if="nameState.status === 'invalid'" class="hint-error">
|
|
|
- 规范化后仍非法(需含字母或数字)
|
|
|
- </span>
|
|
|
- <span v-else-if="nameState.status === 'valid'" class="hint-ok">
|
|
|
- ✓ 合法 kebab-case
|
|
|
- </span>
|
|
|
- </div>
|
|
|
</div>
|
|
|
- <div class="wf-field">
|
|
|
+ <div class="wf-field wf-field-grow">
|
|
|
<label class="wf-label">智能体描述</label>
|
|
|
- <n-input v-model:value="form.description" placeholder="请输入智能体描述" style="width: 240px" size="small"
|
|
|
+ <n-input v-model:value="form.description" placeholder="请输入智能体描述" size="small"
|
|
|
:maxlength="500" />
|
|
|
</div>
|
|
|
+ <div class="wf-name-hint">
|
|
|
+ <span v-if="nameState.status === 'will-rename'" class="hint-warn">
|
|
|
+ ⚠ 保存时将重命名:<code>{{ routeName }}</code> → <code>{{ nameState.normalized }}</code>
|
|
|
+ </span>
|
|
|
+ <span v-else-if="nameState.status === 'will-normalize'" class="hint-warn">
|
|
|
+ 将规范化为:<code>{{ nameState.normalized }}</code>
|
|
|
+ </span>
|
|
|
+ <span v-else-if="nameState.status === 'invalid'" class="hint-error">
|
|
|
+ 规范化后仍非法(需含字母或数字)
|
|
|
+ </span>
|
|
|
+ <span v-else-if="nameState.status === 'valid'" class="hint-ok">
|
|
|
+ ✓ 合法 kebab-case
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="toolbar-right">
|
|
|
@@ -1181,6 +1198,12 @@ function applyGraphData(graphData) {
|
|
|
<!-- 节点属性 Tab -->
|
|
|
<div v-show="activeTab === 'props'" class="tab-pane">
|
|
|
<template v-if="selectedData">
|
|
|
+ <!-- 节点名称(可选,画布内唯一) -->
|
|
|
+ <div class="prop-section">
|
|
|
+ <label class="prop-label">节点名称</label>
|
|
|
+ <n-input :value="selectedData?.nodeName || ''" placeholder="可选,留空则仅显示节点类型" size="small"
|
|
|
+ @update:value="v => updateNodeName(v)" />
|
|
|
+ </div>
|
|
|
<!-- 用户输入节点 -->
|
|
|
<template v-if="selectedNode?.type === 'userInput'">
|
|
|
<div class="prop-section">
|
|
|
@@ -1247,6 +1270,12 @@ function applyGraphData(graphData) {
|
|
|
@update:value="nv => { v.label = nv; onFieldChange('inputs', [...selectedData.inputs]) }" />
|
|
|
<button class="text-btn danger" @click="removeLlmField('inputs', i)">删除</button>
|
|
|
</div>
|
|
|
+ <div class="var-form-row mapping-row">
|
|
|
+ <n-select :value="currentMappingValue(v)"
|
|
|
+ :options="[{ label: '自动推断', value: null }, ...inputMappingOptions]" size="small"
|
|
|
+ clearable placeholder="关联方式:自动推断"
|
|
|
+ @update:value="nv => updateInputMapping(v, nv)" />
|
|
|
+ </div>
|
|
|
</div>
|
|
|
<div v-if="!selectedData?.inputs?.length" class="empty-hint">可用 {{ 变量名 }} 引用,或手动添加</div>
|
|
|
</div>
|
|
|
@@ -1310,6 +1339,12 @@ function applyGraphData(graphData) {
|
|
|
@update:value="nv => { v.label = nv; onFieldChange('inputs', [...selectedData.inputs]) }" />
|
|
|
<button class="text-btn danger" @click="removeAgentField('inputs', i)">删除</button>
|
|
|
</div>
|
|
|
+ <div class="var-form-row mapping-row">
|
|
|
+ <n-select :value="currentMappingValue(v)"
|
|
|
+ :options="[{ label: '自动推断', value: null }, ...inputMappingOptions]" size="small"
|
|
|
+ clearable placeholder="关联方式:自动推断"
|
|
|
+ @update:value="nv => updateInputMapping(v, nv)" />
|
|
|
+ </div>
|
|
|
</div>
|
|
|
<div v-if="!selectedData?.inputs?.length" class="empty-hint">选择技能后自动填充</div>
|
|
|
</div>
|
|
|
@@ -1373,6 +1408,12 @@ function applyGraphData(graphData) {
|
|
|
@update:value="nv => { v.label = nv; onFieldChange('inputs', [...selectedData.inputs]) }" />
|
|
|
<button class="text-btn danger" @click="removeAgentField('inputs', i)">删除</button>
|
|
|
</div>
|
|
|
+ <div class="var-form-row mapping-row">
|
|
|
+ <n-select :value="currentMappingValue(v)"
|
|
|
+ :options="[{ label: '自动推断', value: null }, ...inputMappingOptions]" size="small"
|
|
|
+ clearable placeholder="关联方式:自动推断"
|
|
|
+ @update:value="nv => updateInputMapping(v, nv)" />
|
|
|
+ </div>
|
|
|
</div>
|
|
|
<div v-if="!selectedData?.inputs?.length" class="empty-hint">选择技能后自动填充</div>
|
|
|
</div>
|
|
|
@@ -1443,6 +1484,12 @@ function applyGraphData(graphData) {
|
|
|
@update:value="nv => { v.label = nv; onFieldChange('inputs', [...selectedData.inputs]) }" />
|
|
|
<button class="text-btn danger" @click="removeAgentField('inputs', i)">删除</button>
|
|
|
</div>
|
|
|
+ <div class="var-form-row mapping-row">
|
|
|
+ <n-select :value="currentMappingValue(v)"
|
|
|
+ :options="[{ label: '自动推断', value: null }, ...inputMappingOptions]" size="small"
|
|
|
+ clearable placeholder="关联方式:自动推断"
|
|
|
+ @update:value="nv => updateInputMapping(v, nv)" />
|
|
|
+ </div>
|
|
|
</div>
|
|
|
<div v-if="!selectedData?.inputs?.length" class="empty-hint">可用 {{ 变量名 }} 引用,或手动添加</div>
|
|
|
</div>
|
|
|
@@ -1599,6 +1646,12 @@ function applyGraphData(graphData) {
|
|
|
@update:value="nv => { v.label = nv; onFieldChange('inputs', [...selectedData.inputs]) }" />
|
|
|
<button class="text-btn danger" @click="removeConditionInput(i)">删除</button>
|
|
|
</div>
|
|
|
+ <div class="var-form-row mapping-row">
|
|
|
+ <n-select :value="currentMappingValue(v)"
|
|
|
+ :options="[{ label: '自动推断', value: null }, ...inputMappingOptions]" size="small"
|
|
|
+ clearable placeholder="关联方式:自动推断"
|
|
|
+ @update:value="nv => updateInputMapping(v, nv)" />
|
|
|
+ </div>
|
|
|
</div>
|
|
|
<div v-if="!selectedData?.inputs?.length" class="empty-hint">连接上游节点后自动推断,或手动添加</div>
|
|
|
<div class="passthrough-hint">所有前置数据将透传至下游节点</div>
|
|
|
@@ -1628,23 +1681,6 @@ function applyGraphData(graphData) {
|
|
|
<div v-if="!selectedData?.fields?.length" class="empty-hint">连接上游节点后自动推断,或手动添加</div>
|
|
|
</div>
|
|
|
</template>
|
|
|
- <!-- 输入变量关联 -->
|
|
|
- <template v-if="selectedData?.inputs?.length">
|
|
|
- <div class="prop-section">
|
|
|
- <div class="prop-header">
|
|
|
- <span class="prop-label">输入变量关联</span>
|
|
|
- <span class="empty-hint">未选择时将使用自动推断</span>
|
|
|
- </div>
|
|
|
- <div v-for="(v, i) in selectedData.inputs" :key="'map-' + i" class="var-form">
|
|
|
- <div class="var-form-row">
|
|
|
- <span class="mapping-field" style="flex:1;min-width:80px">{{ v.name }}</span>
|
|
|
- <n-select :value="currentMappingValue(v)"
|
|
|
- :options="[{ label: '自动推断', value: null }, ...inputMappingOptions]" size="small" style="flex:2"
|
|
|
- clearable placeholder="自动推断" @update:value="nv => updateInputMapping(v, nv)" />
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </template>
|
|
|
</template>
|
|
|
<!-- 边映射信息 -->
|
|
|
<template v-else-if="selectedEdge?.data">
|
|
|
@@ -1910,7 +1946,7 @@ function applyGraphData(graphData) {
|
|
|
height: calc(100vh - var(--header-height));
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
- margin: -24px -32px 0;
|
|
|
+ margin: -28px -32px;
|
|
|
background: #13111a;
|
|
|
}
|
|
|
|
|
|
@@ -1931,6 +1967,12 @@ function applyGraphData(graphData) {
|
|
|
gap: 10px;
|
|
|
}
|
|
|
|
|
|
+/* 让左侧工具栏占满 toolbar 中间空间,使描述输入框可扩展到版本历史按钮左侧 */
|
|
|
+.toolbar-left {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0;
|
|
|
+}
|
|
|
+
|
|
|
/* 名称 5 态机提示 */
|
|
|
.name-status-hint {
|
|
|
font-size: 11px;
|
|
|
@@ -1952,17 +1994,44 @@ function applyGraphData(graphData) {
|
|
|
color: #22c55e;
|
|
|
}
|
|
|
|
|
|
-/* 工具栏表单(参考 SkillEdit 的 .form-group / .form-label / .label-hint / .name-hint 风格) */
|
|
|
+/* 工具栏表单:grid 布局让名称提示跨名称、描述两列延伸到描述字段右边界 */
|
|
|
.toolbar-form {
|
|
|
- display: flex;
|
|
|
- gap: 14px;
|
|
|
- align-items: flex-start;
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: auto auto 1fr;
|
|
|
+ column-gap: 14px;
|
|
|
+ row-gap: 4px;
|
|
|
+ align-items: start;
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0;
|
|
|
}
|
|
|
|
|
|
.wf-field {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
gap: 4px;
|
|
|
+ min-width: 0;
|
|
|
+}
|
|
|
+
|
|
|
+/* 让 wf-field 内的 n-input 拉伸到 field 宽度(默认 inline-flex 不会自动占满) */
|
|
|
+.wf-field :deep(.n-input) {
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+/* 三列定位:展示名称 / 名称 / 描述(描述占满剩余空间) */
|
|
|
+.wf-field-show {
|
|
|
+ grid-column: 1;
|
|
|
+ grid-row: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.wf-field-name {
|
|
|
+ grid-column: 2;
|
|
|
+ grid-row: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.wf-field.wf-field-grow {
|
|
|
+ grid-column: 3;
|
|
|
+ grid-row: 1;
|
|
|
+ min-width: 0;
|
|
|
}
|
|
|
|
|
|
.wf-label {
|
|
|
@@ -1979,13 +2048,14 @@ function applyGraphData(graphData) {
|
|
|
}
|
|
|
|
|
|
.wf-name-hint {
|
|
|
+ grid-column: 2 / -1;
|
|
|
+ grid-row: 2;
|
|
|
font-size: 11px;
|
|
|
min-height: 14px;
|
|
|
line-height: 1.2;
|
|
|
white-space: nowrap;
|
|
|
overflow: hidden;
|
|
|
text-overflow: ellipsis;
|
|
|
- max-width: 220px;
|
|
|
}
|
|
|
|
|
|
.wf-name-hint code {
|
|
|
@@ -2604,6 +2674,13 @@ function applyGraphData(graphData) {
|
|
|
margin-bottom: 6px;
|
|
|
}
|
|
|
|
|
|
+/* 关联方式行:作为前置数据的一个属性,紧跟字段定义 */
|
|
|
+.var-form-row.mapping-row {
|
|
|
+ margin-top: 2px;
|
|
|
+ padding-top: 6px;
|
|
|
+ border-top: 1px dashed rgba(255, 255, 255, 0.06);
|
|
|
+}
|
|
|
+
|
|
|
.cond-form {
|
|
|
margin-bottom: 8px;
|
|
|
}
|