Skip to content

Commit 13b4e1d

Browse files
feat: reply node
1 parent c5ae19d commit 13b4e1d

51 files changed

Lines changed: 1563 additions & 522 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

ui/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
"@types/lodash": "^4.17.25",
6060
"@types/node": "^24.13.2",
6161
"@types/nprogress": "^0.2.3",
62+
"@types/sortablejs": "^1.15.9",
6263
"@vitejs/plugin-vue": "^6.0.7",
6364
"@vitejs/plugin-vue-jsx": "^5.1.5",
6465
"@vue/eslint-config-typescript": "^14.8.0",

ui/src/api/enums/knowledge.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
/** 后端知识库类型。 */
22
// TODO v3不再用数字表示 待修改
33
export const KNOWLEDGE_TYPE = { BASE: 0, WEB: 1, LARK: 2, YUQUE: 3, WORKFLOW: 4 } as const
4+
5+
/** 知识库分段检索模式。 */
6+
export const KNOWLEDGE_SEARCH_MODE = { EMBEDDING: 'embedding', KEYWORDS: 'keywords', BLEND: 'blend' } as const
Lines changed: 4 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading

ui/src/components/COMPONENT_README.md

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,12 @@ src/components/
4040
│ │ ├── MoveToDialog.vue
4141
│ │ ├── VirtualizedTree.vue
4242
│ │ └── types.ts
43+
│ ├── knowledge-selection-dialog/
44+
│ │ ├── index.vue # 关联知识库选择、文件夹与共享资源查询
45+
│ │ └── types.ts # 已选知识库快照类型
4346
│ ├── model-select/
44-
│ │ └── index.vue # 按供应商分组的模型选择器
47+
│ │ ├── index.vue # 按供应商分组的模型选择器与可选参数按钮
48+
│ │ └── ModelParamsDialog.vue # 模型参数动态表单弹窗
4549
│ ├── workspace-dropdown/
4650
│ │ └── index.vue # 工作空间选择下拉框
4751
│ └── workspace-relation-tags/
@@ -1035,18 +1039,39 @@ import FolderTree from '@/components/business/folder-tree/index.vue'
10351039
<MoveToDialog ref="moveToDialogRef" :loading="submitting" :source="RESOURCE_TYPE.TOOL" @submit="handleMoveFolder" />
10361040
```
10371041

1042+
### KnowledgeSelectionDialog
1043+
1044+
关联知识库选择弹窗,手动导入 `@/components/business/knowledge-selection-dialog/index.vue`
1045+
通过 `open(knowledge)` 传入已选知识库快照,确认后通过 `submit` 返回新的选择;取消不修改调用方
1046+
数据,关闭后统一清理临时状态。`KnowledgeSelection` 类型允许仅包含 ID,以兼容缺少详情的旧数据。
1047+
组件复用只读 `FolderTree``MkInfiniteScroll` 查询工作空间及共享知识库,支持按名称搜索,
1048+
跨文件夹、搜索和分页保留选择,并限制新选知识库使用相同的 Embedding 模型。固定业务请求由
1049+
该组件负责,调用方维护最终关联 ID 和快照。
1050+
10381051
### ModelSelect
10391052

1040-
按供应商分组展示模型,通过 `v-model` 控制模型 ID,并在选择变化时触发 `change``options` 使用
1041-
供应商标识作为键、模型数组作为值;组件根据 `modelType` 调用供应商接口补充分组名称和图标,
1042-
模型列表仍由使用方按当前业务上下文查询。
1053+
按供应商分组展示模型,通过 `v-model` 控制模型 ID,并在选择变化时触发 `change``options`
1054+
`ModelItem[]``providerOptions``ModelProviderItem[]`,模型列表和供应商列表均由使用方查询。
1055+
1056+
`showModelParams` 默认为 `false`,设为 `true` 时显示参数按钮,设为 `false` 时隐藏。
1057+
未选择模型或传入 `disabled` 时按钮禁用;点击打开组件内的 `ModelParamsDialog`
1058+
通过 `v-model:model-params` 绑定参数:切换模型时加载默认值,清空模型时清空参数,弹窗确认后
1059+
回写配置,取消不修改已保存参数。参数表单沿用上层提供的 `getModelParamsForm(modelId)` 注入方法;
1060+
隐藏参数入口时,组件只处理模型选择,不加载或修改参数。
10431061

10441062
```vue
10451063
<script setup lang="ts">
10461064
import ModelSelect from '@/components/business/model-select/index.vue'
10471065
</script>
10481066
1049-
<ModelSelect v-model="selectedModelId" model-type="LLM" :options="modelOptions" placeholder="请选择模型" @change="handleModelChange" />
1067+
<ModelSelect
1068+
v-model="selectedModelId"
1069+
v-model:model-params="modelParams"
1070+
show-model-params
1071+
:options="modelOptions"
1072+
:provider-options="providerOptions"
1073+
placeholder="请选择模型"
1074+
/>
10501075
```
10511076

10521077
### WorkspaceDropdown
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
<script setup lang="ts">
2+
import { computed, ref, useTemplateRef } from 'vue'
3+
import { cloneDeep } from 'lodash'
4+
import { Refresh } from '@element-plus/icons-vue'
5+
import KnowledgeApi from '@/api/admin/workspace/knowledge/knowledge'
6+
import SharedApi from '@/api/admin/workspace/shared'
7+
import type { ParamsPage } from '@/api/admin/core/types'
8+
import type { FolderItem, KnowledgeItem } from '@/api/types'
9+
import { RESOURCE_TYPE } from '@/api/enums'
10+
import { FOLDER_ENTRIES, FOLDER_ENTRY_ID } from '@/constants/folder'
11+
import FolderTree from '@/components/business/folder-tree/index.vue'
12+
import type { KnowledgeSelection } from './types'
13+
14+
defineOptions({ name: 'KnowledgeSelectionDialog' })
15+
16+
const emit = defineEmits<{ submit: [knowledge: KnowledgeSelection[]] }>()
17+
const visible = ref(false)
18+
const searchKeyword = ref('')
19+
const currentFolder = ref<FolderItem>({ ...FOLDER_ENTRIES[RESOURCE_TYPE.KNOWLEDGE].all })
20+
const knowledgeOptions = ref<KnowledgeItem[]>([])
21+
const selectedKnowledge = ref<KnowledgeSelection[]>([])
22+
const infiniteScrollRef = useTemplateRef<{ reset: () => Promise<void> }>('infiniteScrollRef')
23+
const folderTreeRef = useTemplateRef<InstanceType<typeof FolderTree>>('folderTreeRef')
24+
let dialogVersion = 0
25+
26+
// 以已选知识库确定 Embedding 模型,跨文件夹、搜索和分页保留选择。
27+
const embeddingModelId = computed(() => selectedKnowledge.value.find((knowledge) => knowledge.embedding_model_id)?.embedding_model_id)
28+
29+
function isSelected(id: string) {
30+
return selectedKnowledge.value.some((knowledge) => knowledge.id === id)
31+
}
32+
33+
function isDisabled(knowledge: KnowledgeItem) {
34+
return !isSelected(knowledge.id) && Boolean(embeddingModelId.value && knowledge.embedding_model_id !== embeddingModelId.value)
35+
}
36+
37+
function toggleKnowledge(knowledge: KnowledgeItem) {
38+
if (isDisabled(knowledge)) return
39+
selectedKnowledge.value = isSelected(knowledge.id)
40+
? selectedKnowledge.value.filter(({ id }) => id !== knowledge.id)
41+
: [...selectedKnowledge.value, cloneDeep(knowledge)]
42+
}
43+
44+
function loadKnowledgePage(pagination: ParamsPage) {
45+
const version = dialogVersion
46+
const shared = currentFolder.value.id === FOLDER_ENTRY_ID.SHARED
47+
const requestApi = shared ? SharedApi : KnowledgeApi
48+
return requestApi
49+
.getKnowledgePage(pagination, {
50+
...(shared ? {} : { folder_id: currentFolder.value.id }),
51+
...(searchKeyword.value.trim() ? { name: searchKeyword.value.trim() } : {}),
52+
})
53+
.then((page) => {
54+
if (version === dialogVersion) {
55+
// 仅补全已选快照,不因当前页或查询结果缺少某个 ID 而删除关联。
56+
const knowledgeById = new Map(page.records.map((knowledge) => [knowledge.id, knowledge]))
57+
selectedKnowledge.value = selectedKnowledge.value.map((knowledge) => knowledgeById.get(knowledge.id) ?? knowledge)
58+
}
59+
return page
60+
})
61+
}
62+
63+
function refreshKnowledge() {
64+
return infiniteScrollRef.value?.reset()
65+
}
66+
67+
function refreshResources() {
68+
folderTreeRef.value?.refresh()
69+
void refreshKnowledge()
70+
}
71+
72+
function selectFolder(folder?: FolderItem) {
73+
if (!folder || folder.id === currentFolder.value.id) return
74+
currentFolder.value = folder
75+
void refreshKnowledge()
76+
}
77+
78+
// 弹窗只在确认后向调用方提交,取消和关闭统一清理临时状态。
79+
function resetData() {
80+
dialogVersion++
81+
searchKeyword.value = ''
82+
currentFolder.value = { ...FOLDER_ENTRIES[RESOURCE_TYPE.KNOWLEDGE].all }
83+
knowledgeOptions.value = []
84+
selectedKnowledge.value = []
85+
}
86+
87+
function open(knowledge: KnowledgeSelection[]) {
88+
resetData()
89+
selectedKnowledge.value = cloneDeep(knowledge)
90+
visible.value = true
91+
}
92+
93+
function submit() {
94+
emit('submit', cloneDeep(selectedKnowledge.value))
95+
visible.value = false
96+
}
97+
98+
defineExpose({ open })
99+
</script>
100+
101+
<template>
102+
<MkDialog v-model="visible" title="添加关联知识库" width="960" @closed="resetData">
103+
<template #subtitle>所选知识库必须使用相同的 Embedding 模型</template>
104+
<div class="flex h-105 gap-4">
105+
<div class="w-55 shrink-0 border-r">
106+
<FolderTree ref="folderTreeRef" :can-edit="false" :source="RESOURCE_TYPE.KNOWLEDGE" @loaded="selectFolder" @select="selectFolder" />
107+
</div>
108+
<div class="flex min-w-0 flex-1 flex-col gap-3">
109+
<div class="flex-between gap-3">
110+
<h6 class="truncate" :title="currentFolder.name">{{ currentFolder.name }}</h6>
111+
<div class="flex items-center gap-2">
112+
<MkSearchInput v-model="searchKeyword" placeholder="搜索知识库" @change="refreshKnowledge" />
113+
<el-button text title="刷新" @click="refreshResources"><MkIcon :icon="Refresh" /></el-button>
114+
</div>
115+
</div>
116+
<el-scrollbar class="flex-1">
117+
<MkInfiniteScroll ref="infiniteScrollRef" v-model="knowledgeOptions" :load="loadKnowledgePage">
118+
<div class="grid grid-cols-2 gap-2 pr-2">
119+
<el-checkbox
120+
v-for="knowledge in knowledgeOptions"
121+
:key="knowledge.id"
122+
:model-value="isSelected(knowledge.id)"
123+
:disabled="isDisabled(knowledge)"
124+
class="m-0! h-auto! min-w-0 rounded-md border p-3! [&_.el-checkbox__label]:min-w-0"
125+
@change="toggleKnowledge(knowledge)"
126+
>
127+
<span class="flex min-w-0 items-center gap-2">
128+
<KnowledgeIcon :type="knowledge.type" :size="24" class="shrink-0" />
129+
<span class="min-w-0">
130+
<span class="block truncate" :title="knowledge.name">{{ knowledge.name }}</span>
131+
<span v-if="knowledge.desc" class="block truncate text-sm text-N600" :title="knowledge.desc">{{ knowledge.desc }}</span>
132+
</span>
133+
</span>
134+
</el-checkbox>
135+
</div>
136+
<template #empty><MkEmpty :type="searchKeyword ? 'search' : 'default'" /></template>
137+
</MkInfiniteScroll>
138+
</el-scrollbar>
139+
</div>
140+
</div>
141+
<template #footer>
142+
<div class="flex-between">
143+
<div class="flex items-center gap-2">
144+
<span class="text-N600">已选择 {{ selectedKnowledge.length }} 项</span>
145+
<el-button v-if="selectedKnowledge.length" link type="primary" @click="selectedKnowledge = []">清空</el-button>
146+
</div>
147+
<div>
148+
<el-button @click="visible = false">取消</el-button>
149+
<el-button type="primary" @click="submit">确定</el-button>
150+
</div>
151+
</div>
152+
</template>
153+
</MkDialog>
154+
</template>
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import type { KnowledgeItem } from '@/api/types'
2+
3+
/** 已选知识库允许只保留 ID,避免旧工作流缺少快照时丢失关联。 */
4+
export type KnowledgeSelection = Pick<KnowledgeItem, 'id'> & Partial<KnowledgeItem>

ui/src/workflow-canvas/nodes/base-node/ModelParamsDialog.vue renamed to ui/src/components/business/model-select/ModelParamsDialog.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { cloneDeep } from 'lodash'
44
import type { DynamicFormField } from '@/api/types'
55
import { MkDynamicsForm, type FormField } from '@/components/mk-dynamics-form'
66
7-
defineOptions({ name: 'BaseNodeModelParamsDialog' })
7+
defineOptions({ name: 'ModelParamsDialog' })
88
99
const emit = defineEmits<{ submit: [settings: Record<string, unknown>] }>()
1010

0 commit comments

Comments
 (0)