Преглед изворни кода

1. 实现了工作流节点字段「引入」功能,前置数据可一键从前驱节点输出中选取、输出变量可一键从后继节点输入中选取,自动填充变量名、中文名、类型、描述并建立关联关系;
2. 新增了工作流节点前置数据与输出变量的「描述」字段,与「用户输入」节点保持一致,覆盖 LLM、智能体、技能、条件分支、输出和知识库检索六类节点;
3. 实现了模板变量动态同步机制,移除「从模板同步」按钮,改为输入 systemPrompt / userPrompt / actionPrompt / query / 条件表达式时实时提取 {{变量名}} 并追加到前置数据,覆盖 LLM、智能操作、知识库检索、条件分支四类节点;
4. 补充了知识库检索节点的前置数据与输出变量编辑面板,默认输出 evidences / evidenceCount / sourceType / diagnostics 四个字段,并支持手动维护输入;
5. 统一了「用户输入」节点在画布中的变量标签样式,由圆角彩色标签改为与其他节点一致的等宽字体矩形标签,消除视觉差异;
6. 扩展了 ioInference 工具,新增 getReachableSuccessors 正向 BFS 函数与 smartAction 节点的输入推断(优先手动 inputs,回退从 actionPrompt 提取变量),支撑字段引入与 IO 推断;
7. 实现了翻译结果持久化缓存,新增 TranslationEntry 实体与 TranslationCacheRepository(translations.json),应用启动时全量预热到内存,LLM 翻译成功后同步落盘,避免重启后重复调用翻译接口;
8. 更新了 prompt.md 需求记录。

weisijie пре 1 месец
родитељ
комит
7afd8c96b5

+ 23 - 0
backend/src/main/java/com/agent/management/model/entity/TranslationEntry.java

@@ -0,0 +1,23 @@
+package com.agent.management.model.entity;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 翻译缓存条目 — 持久化到 translations.json
+ * sourceText 为业务 key(原文 trim 后值),translatedText 为译文
+ */
+@Data
+public class TranslationEntry {
+
+    private Long id;
+
+    /** 原文(已 trim),业务 key */
+    private String sourceText;
+
+    /** 译文 */
+    private String translatedText;
+
+    private LocalDateTime createdAt;
+}

+ 82 - 0
backend/src/main/java/com/agent/management/repository/TranslationCacheRepository.java

@@ -0,0 +1,82 @@
+package com.agent.management.repository;
+
+import com.agent.management.model.entity.TranslationEntry;
+import com.agent.management.repository.json.AbstractJsonRepository;
+import com.agent.management.repository.json.JsonStore;
+import com.fasterxml.jackson.core.type.TypeReference;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * 翻译缓存持久化仓库
+ *
+ * 文件:translations.json
+ * 业务 key:sourceText(原文 trim 后值),全文精确匹配
+ *
+ * 写入并发说明:
+ * 不同 sourceText 的并发 save 在 AbstractJsonRepository.save 中存在
+ * loadAll→writeAll 之间的覆盖窗口(不同文件读写锁,不互斥),
+ * 因此本仓库的 save(sourceText, translatedText) 加 synchronized 串行化。
+ * 写频率极低(仅在 LLM 翻译成功时触发),无性能影响。
+ */
+@Component
+public class TranslationCacheRepository extends AbstractJsonRepository<TranslationEntry> {
+
+    public TranslationCacheRepository(JsonStore store) {
+        super(store, new TypeReference<>() {});
+    }
+
+    @Override
+    protected String getFileName() {
+        return "translations.json";
+    }
+
+    @Override
+    protected Long getId(TranslationEntry entity) {
+        return entity.getId();
+    }
+
+    @Override
+    protected void setId(TranslationEntry entity, Long id) {
+        entity.setId(id);
+    }
+
+    /**
+     * 按原文查找缓存条目(全文精确匹配)
+     */
+    public Optional<TranslationEntry> findBySourceText(String sourceText) {
+        if (sourceText == null) return Optional.empty();
+        List<TranslationEntry> all = findAll();
+        return all.stream()
+                .filter(e -> sourceText.equals(e.getSourceText()))
+                .findFirst();
+    }
+
+    /**
+     * 保存翻译结果:存在则更新译文,不存在则新增
+     * 方法级 synchronized 规避并发覆盖窗口
+     */
+    public synchronized void save(String sourceText, String translatedText) {
+        if (sourceText == null || translatedText == null) return;
+
+        List<TranslationEntry> all = findAll();
+        for (TranslationEntry e : all) {
+            if (sourceText.equals(e.getSourceText())) {
+                if (!translatedText.equals(e.getTranslatedText())) {
+                    e.setTranslatedText(translatedText);
+                    writeAll(all);
+                }
+                return;
+            }
+        }
+
+        TranslationEntry entry = new TranslationEntry();
+        entry.setSourceText(sourceText);
+        entry.setTranslatedText(translatedText);
+        entry.setCreatedAt(LocalDateTime.now());
+        save(entry);
+    }
+}

+ 23 - 1
backend/src/main/java/com/agent/management/service/impl/TranslationServiceImpl.java

@@ -1,9 +1,12 @@
 package com.agent.management.service.impl;
 
+import com.agent.management.model.entity.TranslationEntry;
 import com.agent.management.model.vo.TranslationVO;
+import com.agent.management.repository.TranslationCacheRepository;
 import com.agent.management.service.AiModelService;
 import com.agent.management.service.SseService;
 import com.agent.management.service.TranslationService;
+import jakarta.annotation.PostConstruct;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.ai.chat.client.ChatClient;
 import org.springframework.scheduling.annotation.Async;
@@ -30,15 +33,32 @@ public class TranslationServiceImpl implements TranslationService {
 
     private final AiModelService aiModelService;
     private final SseService sseService;
+    private final TranslationCacheRepository cacheRepository;
     /** 已完成的翻译缓存:原文 -> 译文 */
     private final Map<String, String> translationCache = new ConcurrentHashMap<>();
     /** 正在进行中的翻译:原文 -> CompletableFuture<译文> */
     private final Map<String, CompletableFuture<String>> pendingTranslations = new ConcurrentHashMap<>();
 
     public TranslationServiceImpl(AiModelService aiModelService,
-                                  SseService sseService) {
+                                  SseService sseService,
+                                  TranslationCacheRepository cacheRepository) {
         this.aiModelService = aiModelService;
         this.sseService = sseService;
+        this.cacheRepository = cacheRepository;
+    }
+
+    /**
+     * 启动时将 translations.json 全量预热到内存缓存
+     */
+    @PostConstruct
+    void initCacheFromStore() {
+        List<TranslationEntry> all = cacheRepository.findAll();
+        for (TranslationEntry e : all) {
+            if (e.getSourceText() != null && e.getTranslatedText() != null) {
+                translationCache.put(e.getSourceText(), e.getTranslatedText());
+            }
+        }
+        log.info("[翻译] 缓存预热完成,载入 {} 条翻译记录", all.size());
     }
 
     @Override
@@ -153,6 +173,7 @@ public class TranslationServiceImpl implements TranslationService {
             if (translated != null && !translated.trim().isEmpty()) {
                 String result = translated.trim();
                 translationCache.put(key, result);
+                cacheRepository.save(key, result);
                 log.info("[翻译] 翻译成功: {} -> {}", truncate(text, 80), truncate(result, 80));
                 return result;
             }
@@ -203,6 +224,7 @@ public class TranslationServiceImpl implements TranslationService {
 
         String finalResult = result.toString();
         translationCache.put(text.trim(), finalResult);
+        cacheRepository.save(text.trim(), finalResult);
         return finalResult;
     }
 

+ 11 - 11
frontend/src/components/workflow/nodes/InputNode.vue

@@ -11,10 +11,10 @@ const props = defineProps({
   <BaseNode :data="data" :has-target="false" color="#10B981">
     <template #icon><ChatbubbleEllipsesOutline /></template>
     <template #body>
-      <div v-if="data?.variables?.length" class="var-list">
-        <span v-for="v in data.variables" :key="v.name" class="var-tag">{{ v.name }}</span>
+      <div v-if="data?.variables?.length" class="io-summary">
+        <span v-for="v in data.variables" :key="v.name" class="io-tag io-out">{{ v.name }}</span>
       </div>
-      <div v-else class="var-empty">未配置变量</div>
+      <div v-else class="output-hint">未配置变量</div>
     </template>
   </BaseNode>
 </template>
@@ -24,13 +24,13 @@ export default { name: 'InputNode' }
 </script>
 
 <style scoped>
-.var-list { display: flex; flex-wrap: wrap; gap: 4px; }
-.var-tag {
-  font-size: 11px;
-  padding: 2px 8px;
-  border-radius: 10px;
-  background: rgba(16,185,129,0.15);
-  color: #10B981;
+.io-summary { display: flex; flex-wrap: wrap; gap: 3px; margin-top: 3px; }
+.io-tag {
+  font-size: 10px;
+  padding: 1px 5px;
+  border-radius: 3px;
+  font-family: 'Cascadia Code', 'Fira Code', monospace;
 }
-.var-empty { color: var(--text-tertiary, #666); font-style: italic; }
+.io-out { background: rgba(34,197,94,0.15); color: #4ade80; }
+.output-hint { font-size: 11px; color: var(--text-tertiary, #888); }
 </style>

+ 51 - 21
frontend/src/composables/useNodeFields.js

@@ -41,6 +41,10 @@ export function useNodeFields({
       update.inputs = (skill?.inputs || []).map(f => ({ ...f, mapping: f.mapping ?? null }))
       update.outputs = (skill?.outputs || []).map(f => ({ ...f, mapping: f.mapping ?? null }))
     }
+    // 模板字段变更:实时同步 inputs(追加新变量,不删除已有字段)
+    if (['systemPrompt', 'userPrompt', 'actionPrompt', 'query'].includes(field)) {
+      syncInputsFromTemplate(update)
+    }
     vfUpdateNode(selectedNode.value.id, update)
     syncSelectedNode()
   }
@@ -59,6 +63,47 @@ export function useNodeFields({
     syncSelectedNode()
   }
 
+  // ========== 模板变量 → inputs 实时同步 ==========
+  // 根据节点类型从对应模板字段提取 {{变量名}},追加到 inputs(不删除已有字段)
+  // 用于:LLM(systemPrompt + userPrompt)、SmartAction(actionPrompt)、KnowledgeRetrieval(query)、Condition(conditions[].expression)
+  function syncInputsFromTemplate(update) {
+    if (!selectedNode.value) return
+    // 合并 update 中的最新值,避免读 selectedData 时落后一个字符
+    const d = { ...(selectedData.value || {}), ...update }
+    const type = selectedNode.value.type
+
+    const texts = []
+    if (type === 'llm') {
+      texts.push(d.systemPrompt || '', d.userPrompt || '')
+    } else if (type === 'smartAction') {
+      texts.push(d.actionPrompt || '')
+    } else if (type === 'knowledgeRetrieval') {
+      texts.push(d.query || '')
+    } else if (type === 'condition') {
+      for (const c of (d.conditions || [])) texts.push(c.expression || '')
+    } else {
+      return
+    }
+
+    const extracted = new Set()
+    for (const t of texts) {
+      for (const v of extractTemplateVariables(t)) extracted.add(v)
+    }
+
+    // 合并到 inputs:仅追加未存在的新变量,保留用户已配置的字段
+    const existing = update.inputs || d.inputs || []
+    const existingNames = new Set(existing.map(f => f.name))
+    const merged = [...existing]
+    for (const name of extracted) {
+      if (!existingNames.has(name)) {
+        merged.push({ name, label: name, type: 'string', description: '', mapping: null })
+      }
+    }
+    if (merged.length !== existing.length) {
+      update.inputs = merged
+    }
+  }
+
   // ========== 通用字段增删(适用于 LLM/Agent/Skill/Condition 的 inputs/outputs 列表) ==========
   function addListField(field) {
     if (!selectedData.value) return
@@ -121,26 +166,12 @@ export function useNodeFields({
     const newConditions = selectedData.value.conditions.map((c, i) =>
       i === index ? { ...c, [field]: value } : c
     )
-    vfUpdateNode(selectedNode.value.id, { conditions: newConditions })
-    syncSelectedNode()
-  }
-
-  // ========== LLM 模板变量同步 ==========
-  function syncLlmFieldsFromTemplate(field) {
-    if (!selectedData.value) return
-    const extracted = extractTemplateVariables(
-      (selectedData.value.systemPrompt || '') + '\n' + (selectedData.value.userPrompt || '')
-    )
-    const existing = selectedData.value[field] || []
-    const existingNames = new Set(existing.map(f => f.name))
-    const merged = [...existing]
-    for (const name of extracted) {
-      if (!existingNames.has(name)) {
-        merged.push({ name, label: name, type: 'string', description: '', mapping: null })
-      }
+    const update = { conditions: newConditions }
+    // 条件表达式变更时实时同步 inputs
+    if (field === 'expression') {
+      syncInputsFromTemplate(update)
     }
-    const final = merged.filter(f => extracted.includes(f.name) || f.label !== f.name || existingNames.has(f.name))
-    vfUpdateNode(selectedNode.value.id, { [field]: final })
+    vfUpdateNode(selectedNode.value.id, update)
     syncSelectedNode()
   }
 
@@ -155,7 +186,6 @@ export function useNodeFields({
     removeOutputField,
     addElif,
     removeElif,
-    onConditionChange,
-    syncLlmFieldsFromTemplate
+    onConditionChange
   }
 }

+ 40 - 0
frontend/src/utils/ioInference.js

@@ -108,6 +108,13 @@ export function getNodeInputs(node) {
       return d.inputs || []
     case 'skill':
       return d.inputs || []
+    case 'smartAction': {
+      // 优先使用手动定义的 inputs
+      if (d.inputs && d.inputs.length) return d.inputs
+      // 回退:从操作要求模板提取
+      const saVars = new Set(extractTemplateVariables(d.actionPrompt))
+      return [...saVars].map(name => ({ name, label: name, type: 'string', description: '' }))
+    }
     case 'knowledgeRetrieval': {
       // 优先使用手动定义的 inputs
       if (d.inputs && d.inputs.length) return d.inputs
@@ -336,6 +343,39 @@ export function getReachablePredecessors(targetId, allNodes, allEdges) {
   return result
 }
 
+/**
+ * 获取起始节点的所有可达后继节点(通过边正向 BFS)
+ *
+ * 用于"输出变量引入":节点输出可关联到所有可达后继节点的输入字段。
+ *
+ * @param {string} sourceId - 起始节点 ID
+ * @param {Array} allNodes - 全部节点
+ * @param {Array} allEdges - 全部边
+ * @returns {Array} 可达后继节点列表(按 BFS 发现顺序,不含 sourceId 本身,不重复)
+ */
+export function getReachableSuccessors(sourceId, allNodes, allEdges) {
+  const nodeMap = new Map(allNodes.map(n => [n.id, n]))
+  const visited = new Set([sourceId])
+  const queue = [sourceId]
+  const result = []
+
+  while (queue.length > 0) {
+    const currentId = queue.shift()
+    for (const edge of allEdges) {
+      if (edge.source !== currentId) continue
+      if (visited.has(edge.target)) continue
+      visited.add(edge.target)
+      const targetNode = nodeMap.get(edge.target)
+      if (targetNode) {
+        result.push(targetNode)
+        queue.push(edge.target)
+      }
+    }
+  }
+
+  return result
+}
+
 /**
  * 推断指定边的样式
  */

+ 374 - 59
frontend/src/views/workflow/WorkflowEditor.vue

@@ -4,7 +4,7 @@ import { useRoute, useRouter } from 'vue-router'
 import { VueFlow, useVueFlow } from '@vue-flow/core'
 import { Background } from '@vue-flow/background'
 import {
-  NInput, NSelect, NButton, NIcon, NCheckbox, useMessage
+  NInput, NSelect, NButton, NIcon, NCheckbox, NModal, useMessage
 } from 'naive-ui'
 import {
   ArrowBackOutline, SaveOutline, ChatbubbleEllipsesOutline,
@@ -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, getNodeOutputs, getReachablePredecessors, IO_TYPES } from '../../utils/ioInference'
+import { inferNewEdge, refreshMappingsForNode, getNodeOutputs, getNodeInputs, getReachablePredecessors, getReachableSuccessors, IO_TYPES } from '../../utils/ioInference'
 import { useVersionHistory } from '../../composables/useVersionHistory'
 import { useWorkflowRunner } from '../../composables/useWorkflowRunner'
 import { useNodeFields } from '../../composables/useNodeFields'
@@ -280,7 +280,14 @@ function getDefaultData(type) {
       allGraphSources: false,
       graphSourceIds: [],
       knowledgeBaseId: null,
-      topK: 5
+      topK: 5,
+      inputs: [],
+      outputs: [
+        { name: 'evidences', label: '检索证据列表', type: 'array', description: '命中的知识片段集合', mapping: null },
+        { name: 'evidenceCount', label: '证据数量', type: 'number', description: '命中证据条数', mapping: null },
+        { name: 'sourceType', label: '来源类型', type: 'string', description: 'DOCUMENT / STRUCTURED_DATA / GRAPH / HYBRID', mapping: null },
+        { name: 'diagnostics', label: '诊断信息', type: 'object', description: '检索过程的诊断信息', mapping: null }
+      ]
     }
     case 'condition': return { nodeName: '', label: '条件分支', conditions: [{ type: 'IF', expression: '' }] }
     case 'output': return { nodeName: '', label: '输出', fields: [{ name: 'result', label: '输出结果', type: 'string', description: '' }] }
@@ -444,6 +451,112 @@ function clearInputMapping(input) {
   onFieldChange('inputs', [...selectedData.value.inputs])
 }
 
+// ========== 字段引入对话框:从前驱/后继节点引入字段 ==========
+const importDialog = reactive({
+  open: false,
+  mode: null,    // 'predecessor' | 'successor'
+  candidates: []
+})
+
+// 前驱输出候选项:用于"前置数据"面板的引入按钮
+const predecessorOutputCandidates = computed(() => {
+  if (!selectedNode.value) return []
+  const predecessors = getReachablePredecessors(selectedNode.value.id, getNodes.value, getEdges.value)
+  const result = []
+  for (const src of predecessors) {
+    for (const out of getNodeOutputs(src)) {
+      result.push({
+        nodeId: src.id,
+        nodeName: src.data?.nodeName || src.data?.label || src.id,
+        fieldName: out.name,
+        fieldLabel: out.label || out.name,
+        fieldType: out.type || 'string',
+        fieldDescription: out.description || ''
+      })
+    }
+  }
+  return result
+})
+
+// 后继输入候选项:用于"输出变量"面板的引入按钮
+const successorInputCandidates = computed(() => {
+  if (!selectedNode.value) return []
+  const successors = getReachableSuccessors(selectedNode.value.id, getNodes.value, getEdges.value)
+  const result = []
+  for (const dst of successors) {
+    for (const inp of getNodeInputs(dst)) {
+      result.push({
+        nodeId: dst.id,
+        nodeName: dst.data?.nodeName || dst.data?.label || dst.id,
+        fieldName: inp.name,
+        fieldLabel: inp.label || inp.name,
+        fieldType: inp.type || 'string',
+        fieldDescription: inp.description || ''
+      })
+    }
+  }
+  return result
+})
+
+function openImportDialog(mode) {
+  importDialog.mode = mode
+  importDialog.candidates = mode === 'predecessor' ? predecessorOutputCandidates.value : successorInputCandidates.value
+  importDialog.open = true
+}
+
+// 确认引入:从前驱输出导入到 inputs,或从后继输入导入到 outputs
+function confirmImportField(candidate) {
+  if (!selectedNode.value) return
+  if (importDialog.mode === 'predecessor') {
+    const newInput = {
+      name: candidate.fieldName,
+      label: candidate.fieldLabel || candidate.fieldName,
+      type: candidate.fieldType || 'string',
+      description: candidate.fieldDescription || '',
+      mapping: {
+        sourceNodeId: candidate.nodeId,
+        sourceField: candidate.fieldName,
+        sourcePath: null,
+        autoMapped: false
+      }
+    }
+    const inputs = [...(selectedData.value?.inputs || []), newInput]
+    vfUpdateNode(selectedNode.value.id, { inputs })
+  } else if (importDialog.mode === 'successor') {
+    const newOutput = {
+      name: candidate.fieldName,
+      label: candidate.fieldLabel || candidate.fieldName,
+      type: candidate.fieldType || 'string',
+      description: candidate.fieldDescription || ''
+    }
+    const outputs = [...(selectedData.value?.outputs || []), newOutput]
+    vfUpdateNode(selectedNode.value.id, { outputs })
+    // 同步设置后继节点对应 input 的 mapping,指向本节点该输出字段
+    const successorNode = getNodes.value.find(n => n.id === candidate.nodeId)
+    if (successorNode) {
+      const succInputs = (successorNode.data?.inputs || []).map(inp => {
+        if (inp.name === candidate.fieldName) {
+          return {
+            ...inp,
+            mapping: {
+              sourceNodeId: selectedNode.value.id,
+              sourceField: candidate.fieldName,
+              sourcePath: null,
+              autoMapped: false
+            }
+          }
+        }
+        return inp
+      })
+      vfUpdateNode(candidate.nodeId, { inputs: succInputs })
+    }
+  }
+  // 刷新当前节点关联的边映射,并同步 selectedNode 引用
+  applyEdgeMappingUpdates(refreshMappingsForNode(selectedNode.value.id, getNodes.value, getEdges.value))
+  selectedNode.value = getNodes.value.find(n => n.id === selectedNode.value.id) || selectedNode.value
+  importDialog.open = false
+}
+
 // ========== 节点名称编辑(含重名校验) ==========
 function updateNodeName(value) {
   const trimmed = (value || '').trim()
@@ -579,8 +692,7 @@ const {
   addListField, removeListField,
   addVariable, removeVariable,
   addOutputField, removeOutputField,
-  addElif, removeElif, onConditionChange,
-  syncLlmFieldsFromTemplate
+  addElif, removeElif, onConditionChange
 } = useNodeFields({
   selectedNode,
   selectedData,
@@ -1254,7 +1366,7 @@ function applyGraphData(graphData) {
                   <div class="prop-header">
                     <span class="prop-label">前置数据</span>
                     <div style="display:flex;gap:4px">
-                      <button class="text-btn" @click="syncLlmFieldsFromTemplate('inputs')">从模板同步</button>
+                      <button class="text-btn" @click="openImportDialog('predecessor')">引入</button>
                       <button class="text-btn" @click="addLlmField('inputs')">+ 添加</button>
                     </div>
                   </div>
@@ -1262,19 +1374,21 @@ function applyGraphData(graphData) {
                     <div class="var-form-row">
                       <n-input :value="v.name" placeholder="变量名" size="small"
                         @update:value="nv => { v.name = nv; onFieldChange('inputs', [...selectedData.inputs]) }" />
-                      <n-select :value="v.type" :options="IO_TYPES" size="small" style="width:100px"
-                        @update:value="nv => { v.type = nv; onFieldChange('inputs', [...selectedData.inputs]) }" />
-                    </div>
-                    <div class="var-form-row">
                       <n-input :value="v.label" placeholder="中文名" size="small"
                         @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">
+                      <n-select :value="v.type" :options="IO_TYPES" size="small" style="width:100px"
+                        @update:value="nv => { v.type = nv; onFieldChange('inputs', [...selectedData.inputs]) }" />
+                      <n-input :value="v.description" placeholder="描述" size="small"
+                        @update:value="nv => { v.description = nv; onFieldChange('inputs', [...selectedData.inputs]) }" />
                     </div>
                     <div class="var-form-row mapping-row">
                       <n-select :value="currentMappingValue(v)"
                         :options="[{ label: '自动推断', value: null }, ...inputMappingOptions]" size="small"
-                        clearable placeholder="关联方式:自动推断"
+                        clearable placeholder="关联方式:自动推断" style="flex:1"
                         @update:value="nv => updateInputMapping(v, nv)" />
+                      <button class="text-btn danger" @click="removeLlmField('inputs', i)">删除</button>
                     </div>
                   </div>
                   <div v-if="!selectedData?.inputs?.length" class="empty-hint">可用 {{ 变量名 }} 引用,或手动添加</div>
@@ -1283,18 +1397,25 @@ function applyGraphData(graphData) {
                 <div class="prop-section">
                   <div class="prop-header">
                     <span class="prop-label">输出变量</span>
-                    <button class="text-btn" @click="addLlmField('outputs')">+ 添加</button>
+                    <div style="display:flex;gap:4px">
+                      <button class="text-btn" @click="openImportDialog('successor')">引入</button>
+                      <button class="text-btn" @click="addLlmField('outputs')">+ 添加</button>
+                    </div>
                   </div>
                   <div v-for="(v, i) in selectedData?.outputs || []" :key="'out-' + i" class="var-form">
                     <div class="var-form-row">
                       <n-input :value="v.name" placeholder="变量名" size="small"
                         @update:value="nv => { v.name = nv; onFieldChange('outputs', [...selectedData.outputs]) }" />
+                      <n-input :value="v.label" placeholder="中文名" size="small"
+                        @update:value="nv => { v.label = nv; onFieldChange('outputs', [...selectedData.outputs]) }" />
+                    </div>
+                    <div class="var-form-row">
                       <n-select :value="v.type" :options="IO_TYPES" size="small" style="width:100px"
                         @update:value="nv => { v.type = nv; onFieldChange('outputs', [...selectedData.outputs]) }" />
+                      <n-input :value="v.description" placeholder="描述" size="small"
+                        @update:value="nv => { v.description = nv; onFieldChange('outputs', [...selectedData.outputs]) }" />
                     </div>
-                    <div class="var-form-row">
-                      <n-input :value="v.label" placeholder="中文名" size="small"
-                        @update:value="nv => { v.label = nv; onFieldChange('outputs', [...selectedData.outputs]) }" />
+                    <div class="var-form-row" style="justify-content:flex-end">
                       <button class="text-btn danger" @click="removeLlmField('outputs', i)">删除</button>
                     </div>
                   </div>
@@ -1325,25 +1446,30 @@ function applyGraphData(graphData) {
                 <div class="prop-section">
                   <div class="prop-header">
                     <span class="prop-label">前置数据</span>
-                    <button class="text-btn" @click="addAgentField('inputs')">+ 添加</button>
+                    <div style="display:flex;gap:4px">
+                      <button class="text-btn" @click="openImportDialog('predecessor')">引入</button>
+                      <button class="text-btn" @click="addAgentField('inputs')">+ 添加</button>
+                    </div>
                   </div>
                   <div v-for="(v, i) in selectedData?.inputs || []" :key="'in-' + i" class="var-form">
                     <div class="var-form-row">
                       <n-input :value="v.name" placeholder="变量名" size="small"
                         @update:value="nv => { v.name = nv; onFieldChange('inputs', [...selectedData.inputs]) }" />
-                      <n-select :value="v.type" :options="IO_TYPES" size="small" style="width:100px"
-                        @update:value="nv => { v.type = nv; onFieldChange('inputs', [...selectedData.inputs]) }" />
-                    </div>
-                    <div class="var-form-row">
                       <n-input :value="v.label" placeholder="中文名" size="small"
                         @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">
+                      <n-select :value="v.type" :options="IO_TYPES" size="small" style="width:100px"
+                        @update:value="nv => { v.type = nv; onFieldChange('inputs', [...selectedData.inputs]) }" />
+                      <n-input :value="v.description" placeholder="描述" size="small"
+                        @update:value="nv => { v.description = nv; onFieldChange('inputs', [...selectedData.inputs]) }" />
                     </div>
                     <div class="var-form-row mapping-row">
                       <n-select :value="currentMappingValue(v)"
                         :options="[{ label: '自动推断', value: null }, ...inputMappingOptions]" size="small"
-                        clearable placeholder="关联方式:自动推断"
+                        clearable placeholder="关联方式:自动推断" style="flex:1"
                         @update:value="nv => updateInputMapping(v, nv)" />
+                      <button class="text-btn danger" @click="removeAgentField('inputs', i)">删除</button>
                     </div>
                   </div>
                   <div v-if="!selectedData?.inputs?.length" class="empty-hint">选择技能后自动填充</div>
@@ -1352,18 +1478,25 @@ function applyGraphData(graphData) {
                 <div class="prop-section">
                   <div class="prop-header">
                     <span class="prop-label">输出变量</span>
-                    <button class="text-btn" @click="addAgentField('outputs')">+ 添加</button>
+                    <div style="display:flex;gap:4px">
+                      <button class="text-btn" @click="openImportDialog('successor')">引入</button>
+                      <button class="text-btn" @click="addAgentField('outputs')">+ 添加</button>
+                    </div>
                   </div>
                   <div v-for="(v, i) in selectedData?.outputs || []" :key="'out-' + i" class="var-form">
                     <div class="var-form-row">
                       <n-input :value="v.name" placeholder="变量名" size="small"
                         @update:value="nv => { v.name = nv; onFieldChange('outputs', [...selectedData.outputs]) }" />
+                      <n-input :value="v.label" placeholder="中文名" size="small"
+                        @update:value="nv => { v.label = nv; onFieldChange('outputs', [...selectedData.outputs]) }" />
+                    </div>
+                    <div class="var-form-row">
                       <n-select :value="v.type" :options="IO_TYPES" size="small" style="width:100px"
                         @update:value="nv => { v.type = nv; onFieldChange('outputs', [...selectedData.outputs]) }" />
+                      <n-input :value="v.description" placeholder="描述" size="small"
+                        @update:value="nv => { v.description = nv; onFieldChange('outputs', [...selectedData.outputs]) }" />
                     </div>
-                    <div class="var-form-row">
-                      <n-input :value="v.label" placeholder="中文名" size="small"
-                        @update:value="nv => { v.label = nv; onFieldChange('outputs', [...selectedData.outputs]) }" />
+                    <div class="var-form-row" style="justify-content:flex-end">
                       <button class="text-btn danger" @click="removeAgentField('outputs', i)">删除</button>
                     </div>
                   </div>
@@ -1394,25 +1527,30 @@ function applyGraphData(graphData) {
                 <div class="prop-section">
                   <div class="prop-header">
                     <span class="prop-label">前置数据</span>
-                    <button class="text-btn" @click="addAgentField('inputs')">+ 添加</button>
+                    <div style="display:flex;gap:4px">
+                      <button class="text-btn" @click="openImportDialog('predecessor')">引入</button>
+                      <button class="text-btn" @click="addAgentField('inputs')">+ 添加</button>
+                    </div>
                   </div>
                   <div v-for="(v, i) in selectedData?.inputs || []" :key="'in-' + i" class="var-form">
                     <div class="var-form-row">
                       <n-input :value="v.name" placeholder="变量名" size="small"
                         @update:value="nv => { v.name = nv; onFieldChange('inputs', [...selectedData.inputs]) }" />
-                      <n-select :value="v.type" :options="IO_TYPES" size="small" style="width:100px"
-                        @update:value="nv => { v.type = nv; onFieldChange('inputs', [...selectedData.inputs]) }" />
-                    </div>
-                    <div class="var-form-row">
                       <n-input :value="v.label" placeholder="中文名" size="small"
                         @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">
+                      <n-select :value="v.type" :options="IO_TYPES" size="small" style="width:100px"
+                        @update:value="nv => { v.type = nv; onFieldChange('inputs', [...selectedData.inputs]) }" />
+                      <n-input :value="v.description" placeholder="描述" size="small"
+                        @update:value="nv => { v.description = nv; onFieldChange('inputs', [...selectedData.inputs]) }" />
                     </div>
                     <div class="var-form-row mapping-row">
                       <n-select :value="currentMappingValue(v)"
                         :options="[{ label: '自动推断', value: null }, ...inputMappingOptions]" size="small"
-                        clearable placeholder="关联方式:自动推断"
+                        clearable placeholder="关联方式:自动推断" style="flex:1"
                         @update:value="nv => updateInputMapping(v, nv)" />
+                      <button class="text-btn danger" @click="removeAgentField('inputs', i)">删除</button>
                     </div>
                   </div>
                   <div v-if="!selectedData?.inputs?.length" class="empty-hint">选择技能后自动填充</div>
@@ -1421,18 +1559,25 @@ function applyGraphData(graphData) {
                 <div class="prop-section">
                   <div class="prop-header">
                     <span class="prop-label">输出变量</span>
-                    <button class="text-btn" @click="addAgentField('outputs')">+ 添加</button>
+                    <div style="display:flex;gap:4px">
+                      <button class="text-btn" @click="openImportDialog('successor')">引入</button>
+                      <button class="text-btn" @click="addAgentField('outputs')">+ 添加</button>
+                    </div>
                   </div>
                   <div v-for="(v, i) in selectedData?.outputs || []" :key="'out-' + i" class="var-form">
                     <div class="var-form-row">
                       <n-input :value="v.name" placeholder="变量名" size="small"
                         @update:value="nv => { v.name = nv; onFieldChange('outputs', [...selectedData.outputs]) }" />
+                      <n-input :value="v.label" placeholder="中文名" size="small"
+                        @update:value="nv => { v.label = nv; onFieldChange('outputs', [...selectedData.outputs]) }" />
+                    </div>
+                    <div class="var-form-row">
                       <n-select :value="v.type" :options="IO_TYPES" size="small" style="width:100px"
                         @update:value="nv => { v.type = nv; onFieldChange('outputs', [...selectedData.outputs]) }" />
+                      <n-input :value="v.description" placeholder="描述" size="small"
+                        @update:value="nv => { v.description = nv; onFieldChange('outputs', [...selectedData.outputs]) }" />
                     </div>
-                    <div class="var-form-row">
-                      <n-input :value="v.label" placeholder="中文名" size="small"
-                        @update:value="nv => { v.label = nv; onFieldChange('outputs', [...selectedData.outputs]) }" />
+                    <div class="var-form-row" style="justify-content:flex-end">
                       <button class="text-btn danger" @click="removeAgentField('outputs', i)">删除</button>
                     </div>
                   </div>
@@ -1470,25 +1615,30 @@ function applyGraphData(graphData) {
                 <div class="prop-section">
                   <div class="prop-header">
                     <span class="prop-label">前置数据</span>
-                    <button class="text-btn" @click="addAgentField('inputs')">+ 添加</button>
+                    <div style="display:flex;gap:4px">
+                      <button class="text-btn" @click="openImportDialog('predecessor')">引入</button>
+                      <button class="text-btn" @click="addAgentField('inputs')">+ 添加</button>
+                    </div>
                   </div>
                   <div v-for="(v, i) in selectedData?.inputs || []" :key="'in-' + i" class="var-form">
                     <div class="var-form-row">
                       <n-input :value="v.name" placeholder="变量名" size="small"
                         @update:value="nv => { v.name = nv; onFieldChange('inputs', [...selectedData.inputs]) }" />
-                      <n-select :value="v.type" :options="IO_TYPES" size="small" style="width:100px"
-                        @update:value="nv => { v.type = nv; onFieldChange('inputs', [...selectedData.inputs]) }" />
-                    </div>
-                    <div class="var-form-row">
                       <n-input :value="v.label" placeholder="中文名" size="small"
                         @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">
+                      <n-select :value="v.type" :options="IO_TYPES" size="small" style="width:100px"
+                        @update:value="nv => { v.type = nv; onFieldChange('inputs', [...selectedData.inputs]) }" />
+                      <n-input :value="v.description" placeholder="描述" size="small"
+                        @update:value="nv => { v.description = nv; onFieldChange('inputs', [...selectedData.inputs]) }" />
                     </div>
                     <div class="var-form-row mapping-row">
                       <n-select :value="currentMappingValue(v)"
                         :options="[{ label: '自动推断', value: null }, ...inputMappingOptions]" size="small"
-                        clearable placeholder="关联方式:自动推断"
+                        clearable placeholder="关联方式:自动推断" style="flex:1"
                         @update:value="nv => updateInputMapping(v, nv)" />
+                      <button class="text-btn danger" @click="removeAgentField('inputs', i)">删除</button>
                     </div>
                   </div>
                   <div v-if="!selectedData?.inputs?.length" class="empty-hint">可用 {{ 变量名 }} 引用,或手动添加</div>
@@ -1497,18 +1647,25 @@ function applyGraphData(graphData) {
                 <div class="prop-section">
                   <div class="prop-header">
                     <span class="prop-label">输出变量</span>
-                    <button class="text-btn" @click="addAgentField('outputs')">+ 添加</button>
+                    <div style="display:flex;gap:4px">
+                      <button class="text-btn" @click="openImportDialog('successor')">引入</button>
+                      <button class="text-btn" @click="addAgentField('outputs')">+ 添加</button>
+                    </div>
                   </div>
                   <div v-for="(v, i) in selectedData?.outputs || []" :key="'out-' + i" class="var-form">
                     <div class="var-form-row">
                       <n-input :value="v.name" placeholder="变量名" size="small"
                         @update:value="nv => { v.name = nv; onFieldChange('outputs', [...selectedData.outputs]) }" />
+                      <n-input :value="v.label" placeholder="中文名" size="small"
+                        @update:value="nv => { v.label = nv; onFieldChange('outputs', [...selectedData.outputs]) }" />
+                    </div>
+                    <div class="var-form-row">
                       <n-select :value="v.type" :options="IO_TYPES" size="small" style="width:100px"
                         @update:value="nv => { v.type = nv; onFieldChange('outputs', [...selectedData.outputs]) }" />
+                      <n-input :value="v.description" placeholder="描述" size="small"
+                        @update:value="nv => { v.description = nv; onFieldChange('outputs', [...selectedData.outputs]) }" />
                     </div>
-                    <div class="var-form-row">
-                      <n-input :value="v.label" placeholder="中文名" size="small"
-                        @update:value="nv => { v.label = nv; onFieldChange('outputs', [...selectedData.outputs]) }" />
+                    <div class="var-form-row" style="justify-content:flex-end">
                       <button class="text-btn danger" @click="removeAgentField('outputs', i)">删除</button>
                     </div>
                   </div>
@@ -1611,6 +1768,66 @@ function applyGraphData(graphData) {
                     </div>
                   </div>
                 </template>
+                <!-- 前置数据 -->
+                <div class="prop-section">
+                  <div class="prop-header">
+                    <span class="prop-label">前置数据</span>
+                    <div style="display:flex;gap:4px">
+                      <button class="text-btn" @click="openImportDialog('predecessor')">引入</button>
+                      <button class="text-btn" @click="addListField('inputs')">+ 添加</button>
+                    </div>
+                  </div>
+                  <div v-for="(v, i) in selectedData?.inputs || []" :key="'in-' + i" class="var-form">
+                    <div class="var-form-row">
+                      <n-input :value="v.name" placeholder="变量名" size="small"
+                        @update:value="nv => { v.name = nv; onFieldChange('inputs', [...selectedData.inputs]) }" />
+                      <n-input :value="v.label" placeholder="中文名" size="small"
+                        @update:value="nv => { v.label = nv; onFieldChange('inputs', [...selectedData.inputs]) }" />
+                    </div>
+                    <div class="var-form-row">
+                      <n-select :value="v.type" :options="IO_TYPES" size="small" style="width:100px"
+                        @update:value="nv => { v.type = nv; onFieldChange('inputs', [...selectedData.inputs]) }" />
+                      <n-input :value="v.description" placeholder="描述" size="small"
+                        @update:value="nv => { v.description = nv; onFieldChange('inputs', [...selectedData.inputs]) }" />
+                    </div>
+                    <div class="var-form-row mapping-row">
+                      <n-select :value="currentMappingValue(v)"
+                        :options="[{ label: '自动推断', value: null }, ...inputMappingOptions]" size="small"
+                        clearable placeholder="关联方式:自动推断" style="flex:1"
+                        @update:value="nv => updateInputMapping(v, nv)" />
+                      <button class="text-btn danger" @click="removeListField('inputs', i)">删除</button>
+                    </div>
+                  </div>
+                  <div v-if="!selectedData?.inputs?.length" class="empty-hint">可用 {{ 变量名 }} 引用,或手动添加</div>
+                </div>
+                <!-- 输出变量 -->
+                <div class="prop-section">
+                  <div class="prop-header">
+                    <span class="prop-label">输出变量</span>
+                    <div style="display:flex;gap:4px">
+                      <button class="text-btn" @click="openImportDialog('successor')">引入</button>
+                      <button class="text-btn" @click="addListField('outputs')">+ 添加</button>
+                    </div>
+                  </div>
+                  <div v-for="(v, i) in selectedData?.outputs || []" :key="'out-' + i" class="var-form">
+                    <div class="var-form-row">
+                      <n-input :value="v.name" placeholder="变量名" size="small"
+                        @update:value="nv => { v.name = nv; onFieldChange('outputs', [...selectedData.outputs]) }" />
+                      <n-input :value="v.label" placeholder="中文名" size="small"
+                        @update:value="nv => { v.label = nv; onFieldChange('outputs', [...selectedData.outputs]) }" />
+                    </div>
+                    <div class="var-form-row">
+                      <n-select :value="v.type" :options="IO_TYPES" size="small" style="width:100px"
+                        @update:value="nv => { v.type = nv; onFieldChange('outputs', [...selectedData.outputs]) }" />
+                      <n-input :value="v.description" placeholder="描述" size="small"
+                        @update:value="nv => { v.description = nv; onFieldChange('outputs', [...selectedData.outputs]) }" />
+                    </div>
+                    <div class="var-form-row" style="justify-content:flex-end">
+                      <button class="text-btn danger" @click="removeListField('outputs', i)">删除</button>
+                    </div>
+                  </div>
+                  <div v-if="!selectedData?.outputs?.length" class="empty-hint">默认输出 evidences / evidenceCount / sourceType / diagnostics</div>
+                </div>
               </template>
 
               <!-- 条件分支节点 -->
@@ -1632,25 +1849,30 @@ function applyGraphData(graphData) {
                 <div class="prop-section">
                   <div class="prop-header">
                     <span class="prop-label">前置数据</span>
-                    <button class="text-btn" @click="addConditionInput">+ 添加</button>
+                    <div style="display:flex;gap:4px">
+                      <button class="text-btn" @click="openImportDialog('predecessor')">引入</button>
+                      <button class="text-btn" @click="addConditionInput">+ 添加</button>
+                    </div>
                   </div>
                   <div v-for="(v, i) in selectedData?.inputs || []" :key="i" class="var-form">
                     <div class="var-form-row">
                       <n-input :value="v.name" placeholder="变量名" size="small"
                         @update:value="nv => { v.name = nv; onFieldChange('inputs', [...selectedData.inputs]) }" />
-                      <n-select :value="v.type" :options="IO_TYPES" size="small" style="width:100px"
-                        @update:value="nv => { v.type = nv; onFieldChange('inputs', [...selectedData.inputs]) }" />
-                    </div>
-                    <div class="var-form-row">
                       <n-input :value="v.label" placeholder="中文名" size="small"
                         @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">
+                      <n-select :value="v.type" :options="IO_TYPES" size="small" style="width:100px"
+                        @update:value="nv => { v.type = nv; onFieldChange('inputs', [...selectedData.inputs]) }" />
+                      <n-input :value="v.description" placeholder="描述" size="small"
+                        @update:value="nv => { v.description = nv; onFieldChange('inputs', [...selectedData.inputs]) }" />
                     </div>
                     <div class="var-form-row mapping-row">
                       <n-select :value="currentMappingValue(v)"
                         :options="[{ label: '自动推断', value: null }, ...inputMappingOptions]" size="small"
-                        clearable placeholder="关联方式:自动推断"
+                        clearable placeholder="关联方式:自动推断" style="flex:1"
                         @update:value="nv => updateInputMapping(v, nv)" />
+                      <button class="text-btn danger" @click="removeConditionInput(i)">删除</button>
                     </div>
                   </div>
                   <div v-if="!selectedData?.inputs?.length" class="empty-hint">连接上游节点后自动推断,或手动添加</div>
@@ -1669,12 +1891,16 @@ function applyGraphData(graphData) {
                     <div class="var-form-row">
                       <n-input :value="f.name" placeholder="字段名" size="small"
                         @update:value="nv => { f.name = nv; onFieldChange('fields', [...selectedData.fields]) }" />
+                      <n-input :value="f.label" placeholder="中文名" size="small"
+                        @update:value="nv => { f.label = nv; onFieldChange('fields', [...selectedData.fields]) }" />
+                    </div>
+                    <div class="var-form-row">
                       <n-select :value="f.type" :options="IO_TYPES" size="small" style="width:100px"
                         @update:value="nv => { f.type = nv; onFieldChange('fields', [...selectedData.fields]) }" />
+                      <n-input :value="f.description" placeholder="描述" size="small"
+                        @update:value="nv => { f.description = nv; onFieldChange('fields', [...selectedData.fields]) }" />
                     </div>
-                    <div class="var-form-row">
-                      <n-input :value="f.label" placeholder="中文名" size="small"
-                        @update:value="nv => { f.label = nv; onFieldChange('fields', [...selectedData.fields]) }" />
+                    <div class="var-form-row" style="justify-content:flex-end">
                       <button class="text-btn danger" @click="removeOutputField(i)">删除</button>
                     </div>
                   </div>
@@ -1938,6 +2164,27 @@ function applyGraphData(graphData) {
       </div>
     </Teleport>
 
+    <!-- 字段引入对话框:从前驱输出或后继输入中引入字段 -->
+    <n-modal v-model:show="importDialog.open" preset="card" style="max-width:560px"
+      :title="importDialog.mode === 'predecessor' ? '从前驱节点引入字段' : '向后继节点引入字段'">
+      <div class="import-candidate-list">
+        <div v-if="!importDialog.candidates.length" class="empty-hint">暂无可引入字段</div>
+        <div v-for="(c, i) in importDialog.candidates" :key="i" class="import-candidate"
+          @click="confirmImportField(c)">
+          <div class="candidate-main">
+            <span class="candidate-node">{{ c.nodeName }}</span>
+            <span class="candidate-sep">.</span>
+            <span class="candidate-field">{{ c.fieldName }}</span>
+          </div>
+          <div class="candidate-meta">
+            <span class="candidate-type">{{ c.fieldType }}</span>
+            <span v-if="c.fieldLabel && c.fieldLabel !== c.fieldName" class="candidate-label">{{ c.fieldLabel }}</span>
+          </div>
+          <div v-if="c.fieldDescription" class="candidate-desc">{{ c.fieldDescription }}</div>
+        </div>
+      </div>
+    </n-modal>
+
   </div>
 </template>
 
@@ -2724,6 +2971,74 @@ function applyGraphData(graphData) {
   font-style: italic;
 }
 
+/* 引入对话框候选列表 */
+.import-candidate-list {
+  max-height: 360px;
+  overflow-y: auto;
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+}
+
+.import-candidate {
+  padding: 8px 10px;
+  border-radius: 6px;
+  background: rgba(255, 255, 255, 0.04);
+  border: 1px solid rgba(255, 255, 255, 0.06);
+  cursor: pointer;
+  transition: background 0.15s, border-color 0.15s;
+}
+
+.import-candidate:hover {
+  background: rgba(255, 255, 255, 0.08);
+  border-color: rgba(255, 255, 255, 0.14);
+}
+
+.import-candidate .candidate-main {
+  font-size: 13px;
+  color: var(--text-primary, #eee);
+  margin-bottom: 2px;
+}
+
+.import-candidate .candidate-node {
+  color: var(--text-secondary, #aaa);
+}
+
+.import-candidate .candidate-sep {
+  color: var(--text-tertiary, #666);
+  margin: 0 2px;
+}
+
+.import-candidate .candidate-field {
+  font-family: 'Consolas', 'Monaco', monospace;
+  font-weight: 500;
+}
+
+.import-candidate .candidate-meta {
+  display: flex;
+  gap: 8px;
+  align-items: center;
+  font-size: 11px;
+  margin-bottom: 2px;
+}
+
+.import-candidate .candidate-type {
+  padding: 1px 6px;
+  border-radius: 3px;
+  background: rgba(255, 255, 255, 0.06);
+  color: var(--text-secondary, #aaa);
+}
+
+.import-candidate .candidate-label {
+  color: var(--text-tertiary, #888);
+}
+
+.import-candidate .candidate-desc {
+  font-size: 11px;
+  color: var(--text-tertiary, #777);
+  line-height: 1.4;
+}
+
 .kr-checkbox-row {
   display: flex;
   align-items: center;

+ 21 - 0
prompt.md

@@ -1184,3 +1184,24 @@ java.lang.IllegalStateException: rag-ai-bridge is disabled
 
 ---
 
+工作流编辑处,修改:
+1. “前置数据”和“输出变量”,除变量名、中文名、类型外,增加“描述”,与“用户输入”节点保持一致;
+2. “前置数据”增加一个按钮,“引入”,可在所有前驱节点的输出中作选择,选择后,将对应变量的变量名、中文名、类型和描述填入,“关联关系”自动选择到对应变量;
+3. “输出变量”增加一个按钮,“引入”,可在所有后继节点的输入(前置数据)中作选择,选择后,将对应变量的变量名、中文名、类型和描述填入,后继节点对应变量的“关联关系”自动选择到本节点的该变量;
+4. 部分类型节点(如知识库检索)缺少前置数据和输出变量的设置,补充。
+
+---
+
+工作流部分节点包含“从模板同步”按钮,我希望该功能做成动态的,随着用户的输入,实时识别模板中的{{}}变量,然后动态同步到前置数据中,不再保留按钮。
+另外,将“用户输入”节点在画布中的显示样式修改一下。输入变量的样式与其他节点一致。目前字体和背景都不同。
+
+---
+
+现在,我输入完{{a}}后,要再输入一个字符才能自动识别a这个变量。我希望输入{{}}时不识别,但按顺序输入完{{a}}后或在{{}}中输入a之后,直接识别变量。
+
+---
+
+现在,每次服务启动都需要调用多次翻译接口。我希望将翻译结果缓存下来(到一个json文件里或数据库里),下次翻译时先查询,如果全文匹配则直接调出,节省翻译时间。
+
+---
+