|
|
@@ -4,23 +4,28 @@ import { useRoute, useRouter } from 'vue-router'
|
|
|
import { VueFlow, useVueFlow } from '@vue-flow/core'
|
|
|
import { Background } from '@vue-flow/background'
|
|
|
import {
|
|
|
- NInput, NSelect, NButton, NIcon, useMessage
|
|
|
+ NInput, NSelect, NButton, NIcon, NCheckbox, useMessage
|
|
|
} from 'naive-ui'
|
|
|
import {
|
|
|
ArrowBackOutline, SaveOutline, ChatbubbleEllipsesOutline,
|
|
|
SparklesOutline, RocketOutline, FlaskOutline, GitBranchOutline, ExitOutline,
|
|
|
PlayOutline, CloseOutline, ColorWandOutline, LinkOutline, TrashOutline, OpenOutline,
|
|
|
- TimeOutline
|
|
|
+ TimeOutline, LibraryOutline
|
|
|
} from '@vicons/ionicons5'
|
|
|
import { useWorkflowStore } from '../../stores/workflow'
|
|
|
import { useSkillStore } from '../../stores/skill'
|
|
|
import { runWorkflow, autoAssociate } from '../../api/workflow'
|
|
|
import { getAgentTemplate } from '../../api/agentTemplate'
|
|
|
import { getModelList } from '../../api/model'
|
|
|
+import { getDataSources } from '../../api/datasource'
|
|
|
+import { getGraphSources } from '../../api/graphsource'
|
|
|
+import { getKbDocuments } from '../../api/knowledge'
|
|
|
+import { listKnowledgeBases } from '../../api/rag'
|
|
|
import InputNode from '../../components/workflow/nodes/InputNode.vue'
|
|
|
import LLMNode from '../../components/workflow/nodes/LLMNode.vue'
|
|
|
import AgentNode from '../../components/workflow/nodes/AgentNode.vue'
|
|
|
import SkillNode from '../../components/workflow/nodes/SkillNode.vue'
|
|
|
+import KnowledgeRetrievalNode from '../../components/workflow/nodes/KnowledgeRetrievalNode.vue'
|
|
|
import OutputNode from '../../components/workflow/nodes/OutputNode.vue'
|
|
|
import ConditionNode from '../../components/workflow/nodes/ConditionNode.vue'
|
|
|
import SmartActionNode from '../../components/workflow/nodes/SmartActionNode.vue'
|
|
|
@@ -106,6 +111,7 @@ const {
|
|
|
agent: { color: '#F59E0B', typeLabel: '智能体' },
|
|
|
smartAction: { color: '#EC4899', typeLabel: '智能操作' },
|
|
|
condition: { color: '#F97316', typeLabel: '条件' },
|
|
|
+ knowledgeRetrieval: { color: '#10B981', typeLabel: '知识库检索' },
|
|
|
output: { color: '#EF4444', typeLabel: '输出' }
|
|
|
},
|
|
|
workflowIdRef: workflowId
|
|
|
@@ -192,6 +198,7 @@ const nodeTypes = [
|
|
|
{ type: 'skill', label: '技能', icon: markRaw(FlaskOutline), color: '#06B6D4' },
|
|
|
{ type: 'agent', label: '智能体', icon: markRaw(RocketOutline), color: '#F59E0B' },
|
|
|
{ type: 'smartAction', label: '智能操作', icon: markRaw(ColorWandOutline), color: '#EC4899' },
|
|
|
+ { type: 'knowledgeRetrieval', label: '知识库检索', icon: markRaw(LibraryOutline), color: '#10B981' },
|
|
|
{ type: 'condition', label: '条件分支', icon: markRaw(GitBranchOutline), color: '#F97316' },
|
|
|
{ type: 'output', label: '输出', icon: markRaw(ExitOutline), color: '#EF4444' }
|
|
|
]
|
|
|
@@ -202,6 +209,7 @@ const nodeTypeInfo = {
|
|
|
skill: { color: '#06B6D4', typeLabel: '技能', icon: markRaw(FlaskOutline) },
|
|
|
agent: { color: '#F59E0B', typeLabel: '智能体', icon: markRaw(RocketOutline) },
|
|
|
smartAction: { color: '#EC4899', typeLabel: '智能操作', icon: markRaw(ColorWandOutline) },
|
|
|
+ knowledgeRetrieval: { color: '#10B981', typeLabel: '知识库检索', icon: markRaw(LibraryOutline) },
|
|
|
condition: { color: '#F97316', typeLabel: '条件', icon: markRaw(GitBranchOutline) },
|
|
|
output: { color: '#EF4444', typeLabel: '输出', icon: markRaw(ExitOutline) }
|
|
|
}
|
|
|
@@ -213,6 +221,19 @@ function getDefaultData(type) {
|
|
|
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 'knowledgeRetrieval': return {
|
|
|
+ label: '知识库检索',
|
|
|
+ source: 'document',
|
|
|
+ query: '',
|
|
|
+ allDocuments: true,
|
|
|
+ documentIds: [],
|
|
|
+ allDatasources: false,
|
|
|
+ datasourceIds: [],
|
|
|
+ allGraphSources: false,
|
|
|
+ graphSourceIds: [],
|
|
|
+ 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: '节点' }
|
|
|
@@ -369,6 +390,83 @@ const failStrategyOptions = [
|
|
|
{ label: '跳过继续', value: 'skip' }
|
|
|
]
|
|
|
|
|
|
+// ========== 知识库检索节点:下拉/复选框数据源 ==========
|
|
|
+const krSourceOptions = [
|
|
|
+ { label: '文档数据库', value: 'document' },
|
|
|
+ { label: '结构化数据库', value: 'structured' },
|
|
|
+ { label: '知识图谱库', value: 'graph' },
|
|
|
+ { label: '混合检索', value: 'hybrid' }
|
|
|
+]
|
|
|
+const krDocuments = ref([])
|
|
|
+const krDocumentsLoading = ref(false)
|
|
|
+const krDatasources = ref([])
|
|
|
+const krDatasourcesLoading = ref(false)
|
|
|
+const krGraphSources = ref([])
|
|
|
+const krGraphSourcesLoading = ref(false)
|
|
|
+const krKnowledgeBases = ref([])
|
|
|
+const krKnowledgeBasesLoading = ref(false)
|
|
|
+
|
|
|
+const krDocumentOptions = computed(() => krDocuments.value.map(d => ({ label: d.name || `#${d.id}`, value: String(d.id) })))
|
|
|
+const krDatasourceOptions = computed(() => krDatasources.value.map(d => ({ label: d.name || `#${d.id}`, value: String(d.id) })))
|
|
|
+const krGraphSourceOptions = computed(() => krGraphSources.value.map(d => ({ label: d.name || `#${d.id}`, value: String(d.id) })))
|
|
|
+const krKnowledgeBaseOptions = computed(() => krKnowledgeBases.value.map(kb => ({ label: kb.name || `#${kb.id}`, value: kb.id })))
|
|
|
+
|
|
|
+async function loadKrDocuments() {
|
|
|
+ if (krDocuments.value.length || krDocumentsLoading.value) return
|
|
|
+ krDocumentsLoading.value = true
|
|
|
+ try {
|
|
|
+ const res = await getKbDocuments({ page: 1, size: 1000 })
|
|
|
+ krDocuments.value = res.data?.items || []
|
|
|
+ } catch (e) {
|
|
|
+ message.error('加载文档列表失败')
|
|
|
+ } finally {
|
|
|
+ krDocumentsLoading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+async function loadKrDatasources() {
|
|
|
+ if (krDatasources.value.length || krDatasourcesLoading.value) return
|
|
|
+ krDatasourcesLoading.value = true
|
|
|
+ try {
|
|
|
+ const res = await getDataSources()
|
|
|
+ krDatasources.value = res.data || []
|
|
|
+ } catch (e) {
|
|
|
+ message.error('加载结构化数据源失败')
|
|
|
+ } finally {
|
|
|
+ krDatasourcesLoading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+async function loadKrGraphSources() {
|
|
|
+ if (krGraphSources.value.length || krGraphSourcesLoading.value) return
|
|
|
+ krGraphSourcesLoading.value = true
|
|
|
+ try {
|
|
|
+ const res = await getGraphSources()
|
|
|
+ krGraphSources.value = res.data || []
|
|
|
+ } catch (e) {
|
|
|
+ message.error('加载图谱数据源失败')
|
|
|
+ } finally {
|
|
|
+ krGraphSourcesLoading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+async function loadKrKnowledgeBases() {
|
|
|
+ if (krKnowledgeBases.value.length || krKnowledgeBasesLoading.value) return
|
|
|
+ krKnowledgeBasesLoading.value = true
|
|
|
+ try {
|
|
|
+ krKnowledgeBases.value = await listKnowledgeBases() || []
|
|
|
+ } catch (e) {
|
|
|
+ message.error('加载知识库列表失败')
|
|
|
+ } finally {
|
|
|
+ krKnowledgeBasesLoading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function toggleKrListField(field, value) {
|
|
|
+ if (!selectedData.value) return
|
|
|
+ const list = new Set(selectedData.value[field] || [])
|
|
|
+ if (list.has(value)) list.delete(value)
|
|
|
+ else list.add(value)
|
|
|
+ onFieldChange(field, [...list])
|
|
|
+}
|
|
|
+
|
|
|
// ========== 节点字段操作(来自 composable) ==========
|
|
|
const {
|
|
|
onFieldChange, onModelChange,
|
|
|
@@ -753,6 +851,11 @@ async function saveMessageWrap(v) {
|
|
|
onMounted(async () => {
|
|
|
skillStore.fetchSkills()
|
|
|
fetchDbModels()
|
|
|
+ // 预加载知识库检索节点所需的资源列表(失败不阻塞主流程)
|
|
|
+ loadKrDocuments()
|
|
|
+ loadKrDatasources()
|
|
|
+ loadKrGraphSources()
|
|
|
+ loadKrKnowledgeBases()
|
|
|
if (workflowId.value) {
|
|
|
// 编辑已有工作流
|
|
|
try {
|
|
|
@@ -901,6 +1004,7 @@ function applyGraphData(graphData) {
|
|
|
<template #node-llm="nodeProps"><LLMNode :data="nodeProps.data" /></template>
|
|
|
<template #node-agent="nodeProps"><AgentNode :data="nodeProps.data" /></template>
|
|
|
<template #node-skill="nodeProps"><SkillNode :data="nodeProps.data" /></template>
|
|
|
+ <template #node-knowledgeRetrieval="nodeProps"><KnowledgeRetrievalNode :data="nodeProps.data" /></template>
|
|
|
<template #node-output="nodeProps"><OutputNode :data="nodeProps.data" /></template>
|
|
|
<template #node-condition="nodeProps"><ConditionNode :data="nodeProps.data" /></template>
|
|
|
<template #node-smartAction="nodeProps"><SmartActionNode :data="nodeProps.data" /></template>
|
|
|
@@ -1249,6 +1353,126 @@ function applyGraphData(graphData) {
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
+ <!-- 知识库检索节点 -->
|
|
|
+ <template v-if="selectedNode?.type === 'knowledgeRetrieval'">
|
|
|
+ <div class="prop-section">
|
|
|
+ <label class="prop-label">检索来源</label>
|
|
|
+ <n-select
|
|
|
+ :value="selectedData?.source || 'document'"
|
|
|
+ :options="krSourceOptions"
|
|
|
+ size="small"
|
|
|
+ @update:value="v => onFieldChange('source', v)"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div class="prop-section">
|
|
|
+ <label class="prop-label">检索语句</label>
|
|
|
+ <n-input
|
|
|
+ :value="selectedData?.query"
|
|
|
+ type="textarea" :rows="3"
|
|
|
+ placeholder="输入检索语句,可用 {{变量名}} 引用上游变量"
|
|
|
+ size="small"
|
|
|
+ @update:value="v => onFieldChange('query', v)"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 文档数据库 -->
|
|
|
+ <template v-if="(selectedData?.source || 'document') === 'document'">
|
|
|
+ <div class="prop-section">
|
|
|
+ <label class="kr-checkbox-row">
|
|
|
+ <n-checkbox
|
|
|
+ :checked="!!selectedData?.allDocuments"
|
|
|
+ @update:checked="v => onFieldChange('allDocuments', v)"
|
|
|
+ />
|
|
|
+ <span>在所有文档中检索</span>
|
|
|
+ </label>
|
|
|
+ <div v-if="!selectedData?.allDocuments" class="kr-list">
|
|
|
+ <div v-if="krDocumentsLoading" class="empty-hint">加载中...</div>
|
|
|
+ <div v-else-if="!krDocumentOptions.length" class="empty-hint">暂无文档</div>
|
|
|
+ <label v-for="opt in krDocumentOptions" :key="opt.value" class="kr-checkbox-row">
|
|
|
+ <n-checkbox
|
|
|
+ :checked="(selectedData?.documentIds || []).includes(opt.value)"
|
|
|
+ @update:checked="() => toggleKrListField('documentIds', opt.value)"
|
|
|
+ />
|
|
|
+ <span>{{ opt.label }}</span>
|
|
|
+ </label>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="prop-section">
|
|
|
+ <label class="prop-label">TopK(命中条数)</label>
|
|
|
+ <n-input
|
|
|
+ :value="String(selectedData?.topK ?? 5)"
|
|
|
+ type="text"
|
|
|
+ size="small"
|
|
|
+ @update:value="v => onFieldChange('topK', Math.max(1, parseInt(v) || 5))"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <!-- 结构化数据库 -->
|
|
|
+ <template v-if="selectedData?.source === 'structured'">
|
|
|
+ <div class="prop-section">
|
|
|
+ <label class="kr-checkbox-row">
|
|
|
+ <n-checkbox
|
|
|
+ :checked="!!selectedData?.allDatasources"
|
|
|
+ @update:checked="v => onFieldChange('allDatasources', v)"
|
|
|
+ />
|
|
|
+ <span>在所有数据源中检索</span>
|
|
|
+ </label>
|
|
|
+ <div v-if="!selectedData?.allDatasources" class="kr-list">
|
|
|
+ <div v-if="krDatasourcesLoading" class="empty-hint">加载中...</div>
|
|
|
+ <div v-else-if="!krDatasourceOptions.length" class="empty-hint">暂无数据源</div>
|
|
|
+ <label v-for="opt in krDatasourceOptions" :key="opt.value" class="kr-checkbox-row">
|
|
|
+ <n-checkbox
|
|
|
+ :checked="(selectedData?.datasourceIds || []).includes(opt.value)"
|
|
|
+ @update:checked="() => toggleKrListField('datasourceIds', opt.value)"
|
|
|
+ />
|
|
|
+ <span>{{ opt.label }}</span>
|
|
|
+ </label>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <!-- 知识图谱库 -->
|
|
|
+ <template v-if="selectedData?.source === 'graph'">
|
|
|
+ <div class="prop-section">
|
|
|
+ <label class="kr-checkbox-row">
|
|
|
+ <n-checkbox
|
|
|
+ :checked="!!selectedData?.allGraphSources"
|
|
|
+ @update:checked="v => onFieldChange('allGraphSources', v)"
|
|
|
+ />
|
|
|
+ <span>在所有数据源中检索</span>
|
|
|
+ </label>
|
|
|
+ <div v-if="!selectedData?.allGraphSources" class="kr-list">
|
|
|
+ <div v-if="krGraphSourcesLoading" class="empty-hint">加载中...</div>
|
|
|
+ <div v-else-if="!krGraphSourceOptions.length" class="empty-hint">暂无图谱数据源</div>
|
|
|
+ <label v-for="opt in krGraphSourceOptions" :key="opt.value" class="kr-checkbox-row">
|
|
|
+ <n-checkbox
|
|
|
+ :checked="(selectedData?.graphSourceIds || []).includes(opt.value)"
|
|
|
+ @update:checked="() => toggleKrListField('graphSourceIds', opt.value)"
|
|
|
+ />
|
|
|
+ <span>{{ opt.label }}</span>
|
|
|
+ </label>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <!-- 混合检索 -->
|
|
|
+ <template v-if="selectedData?.source === 'hybrid'">
|
|
|
+ <div class="prop-section">
|
|
|
+ <label class="prop-label">知识库</label>
|
|
|
+ <div v-if="krKnowledgeBasesLoading" class="empty-hint">加载中...</div>
|
|
|
+ <n-select
|
|
|
+ :value="selectedData?.knowledgeBaseId"
|
|
|
+ :options="krKnowledgeBaseOptions"
|
|
|
+ placeholder="选择知识库"
|
|
|
+ size="small"
|
|
|
+ @update:value="v => onFieldChange('knowledgeBaseId', v)"
|
|
|
+ />
|
|
|
+ <div v-if="!krKnowledgeBaseOptions.length && !krKnowledgeBasesLoading" class="empty-hint">暂无知识库</div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </template>
|
|
|
+
|
|
|
<!-- 条件分支节点 -->
|
|
|
<template v-if="selectedNode?.type === 'condition'">
|
|
|
<div class="prop-section">
|
|
|
@@ -2127,6 +2351,21 @@ function applyGraphData(graphData) {
|
|
|
font-style: italic;
|
|
|
}
|
|
|
|
|
|
+.kr-checkbox-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 6px;
|
|
|
+ font-size: 12px;
|
|
|
+ cursor: pointer;
|
|
|
+ margin-bottom: 4px;
|
|
|
+}
|
|
|
+.kr-list {
|
|
|
+ margin-top: 6px;
|
|
|
+ max-height: 240px;
|
|
|
+ overflow-y: auto;
|
|
|
+ padding-right: 4px;
|
|
|
+}
|
|
|
+
|
|
|
.passthrough-hint {
|
|
|
font-size: 11px;
|
|
|
color: var(--text-tertiary, #555);
|