Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"@types/lodash": "^4.17.25",
"@types/node": "^24.13.2",
"@types/nprogress": "^0.2.3",
"@types/sortablejs": "^1.15.9",
"@vitejs/plugin-vue": "^6.0.7",
"@vitejs/plugin-vue-jsx": "^5.1.5",
"@vue/eslint-config-typescript": "^14.8.0",
Expand Down
3 changes: 3 additions & 0 deletions ui/src/api/enums/knowledge.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
/** 后端知识库类型。 */
// TODO v3不再用数字表示 待修改
export const KNOWLEDGE_TYPE = { BASE: 0, WEB: 1, LARK: 2, YUQUE: 3, WORKFLOW: 4 } as const

/** 知识库分段检索模式。 */
export const KNOWLEDGE_SEARCH_MODE = { EMBEDDING: 'embedding', KEYWORDS: 'keywords', BLEND: 'blend' } as const
4 changes: 4 additions & 0 deletions ui/src/assets/workflow/icon_global-variables_color.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions ui/src/assets/workflow/icon_session-variables_color.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
35 changes: 30 additions & 5 deletions ui/src/components/COMPONENT_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,12 @@ src/components/
│ │ ├── MoveToDialog.vue
│ │ ├── VirtualizedTree.vue
│ │ └── types.ts
│ ├── knowledge-selection-dialog/
│ │ ├── index.vue # 关联知识库选择、文件夹与共享资源查询
│ │ └── types.ts # 已选知识库快照类型
│ ├── model-select/
│ │ └── index.vue # 按供应商分组的模型选择器
│ │ ├── index.vue # 按供应商分组的模型选择器与可选参数按钮
│ │ └── ModelParamsDialog.vue # 模型参数动态表单弹窗
│ ├── workspace-dropdown/
│ │ └── index.vue # 工作空间选择下拉框
│ └── workspace-relation-tags/
Expand Down Expand Up @@ -1035,18 +1039,39 @@ import FolderTree from '@/components/business/folder-tree/index.vue'
<MoveToDialog ref="moveToDialogRef" :loading="submitting" :source="RESOURCE_TYPE.TOOL" @submit="handleMoveFolder" />
```

### KnowledgeSelectionDialog

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

### ModelSelect

按供应商分组展示模型,通过 `v-model` 控制模型 ID,并在选择变化时触发 `change`。`options` 使用
供应商标识作为键、模型数组作为值;组件根据 `modelType` 调用供应商接口补充分组名称和图标,
模型列表仍由使用方按当前业务上下文查询。
按供应商分组展示模型,通过 `v-model` 控制模型 ID,并在选择变化时触发 `change`。`options` 为
`ModelItem[]`,`providerOptions` 为 `ModelProviderItem[]`,模型列表和供应商列表均由使用方查询。

`showModelParams` 默认为 `false`,设为 `true` 时显示参数按钮,设为 `false` 时隐藏。
未选择模型或传入 `disabled` 时按钮禁用;点击打开组件内的 `ModelParamsDialog`。
通过 `v-model:model-params` 绑定参数:切换模型时加载默认值,清空模型时清空参数,弹窗确认后
回写配置,取消不修改已保存参数。参数表单沿用上层提供的 `getModelParamsForm(modelId)` 注入方法;
隐藏参数入口时,组件只处理模型选择,不加载或修改参数。

```vue
<script setup lang="ts">
import ModelSelect from '@/components/business/model-select/index.vue'
</script>

<ModelSelect v-model="selectedModelId" model-type="LLM" :options="modelOptions" placeholder="请选择模型" @change="handleModelChange" />
<ModelSelect
v-model="selectedModelId"
v-model:model-params="modelParams"
show-model-params
:options="modelOptions"
:provider-options="providerOptions"
placeholder="请选择模型"
/>
```

### WorkspaceDropdown
Expand Down
154 changes: 154 additions & 0 deletions ui/src/components/business/knowledge-selection-dialog/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
<script setup lang="ts">
import { computed, ref, useTemplateRef } from 'vue'
import { cloneDeep } from 'lodash'
import { Refresh } from '@element-plus/icons-vue'
import KnowledgeApi from '@/api/admin/workspace/knowledge/knowledge'
import SharedApi from '@/api/admin/workspace/shared'
import type { ParamsPage } from '@/api/admin/core/types'
import type { FolderItem, KnowledgeItem } from '@/api/types'
import { RESOURCE_TYPE } from '@/api/enums'
import { FOLDER_ENTRIES, FOLDER_ENTRY_ID } from '@/constants/folder'
import FolderTree from '@/components/business/folder-tree/index.vue'
import type { KnowledgeSelection } from './types'

defineOptions({ name: 'KnowledgeSelectionDialog' })

const emit = defineEmits<{ submit: [knowledge: KnowledgeSelection[]] }>()
const visible = ref(false)
const searchKeyword = ref('')
const currentFolder = ref<FolderItem>({ ...FOLDER_ENTRIES[RESOURCE_TYPE.KNOWLEDGE].all })
const knowledgeOptions = ref<KnowledgeItem[]>([])
const selectedKnowledge = ref<KnowledgeSelection[]>([])
const infiniteScrollRef = useTemplateRef<{ reset: () => Promise<void> }>('infiniteScrollRef')
const folderTreeRef = useTemplateRef<InstanceType<typeof FolderTree>>('folderTreeRef')
let dialogVersion = 0

// 以已选知识库确定 Embedding 模型,跨文件夹、搜索和分页保留选择。
const embeddingModelId = computed(() => selectedKnowledge.value.find((knowledge) => knowledge.embedding_model_id)?.embedding_model_id)

function isSelected(id: string) {
return selectedKnowledge.value.some((knowledge) => knowledge.id === id)
}

function isDisabled(knowledge: KnowledgeItem) {
return !isSelected(knowledge.id) && Boolean(embeddingModelId.value && knowledge.embedding_model_id !== embeddingModelId.value)
}

function toggleKnowledge(knowledge: KnowledgeItem) {
if (isDisabled(knowledge)) return
selectedKnowledge.value = isSelected(knowledge.id)
? selectedKnowledge.value.filter(({ id }) => id !== knowledge.id)
: [...selectedKnowledge.value, cloneDeep(knowledge)]
}

function loadKnowledgePage(pagination: ParamsPage) {
const version = dialogVersion
const shared = currentFolder.value.id === FOLDER_ENTRY_ID.SHARED
const requestApi = shared ? SharedApi : KnowledgeApi
return requestApi
.getKnowledgePage(pagination, {
...(shared ? {} : { folder_id: currentFolder.value.id }),
...(searchKeyword.value.trim() ? { name: searchKeyword.value.trim() } : {}),
})
.then((page) => {
if (version === dialogVersion) {
// 仅补全已选快照,不因当前页或查询结果缺少某个 ID 而删除关联。
const knowledgeById = new Map(page.records.map((knowledge) => [knowledge.id, knowledge]))
selectedKnowledge.value = selectedKnowledge.value.map((knowledge) => knowledgeById.get(knowledge.id) ?? knowledge)
}
return page
})
}

function refreshKnowledge() {
return infiniteScrollRef.value?.reset()
}

function refreshResources() {
folderTreeRef.value?.refresh()
void refreshKnowledge()
}

function selectFolder(folder?: FolderItem) {
if (!folder || folder.id === currentFolder.value.id) return
currentFolder.value = folder
void refreshKnowledge()
}

// 弹窗只在确认后向调用方提交,取消和关闭统一清理临时状态。
function resetData() {
dialogVersion++
searchKeyword.value = ''
currentFolder.value = { ...FOLDER_ENTRIES[RESOURCE_TYPE.KNOWLEDGE].all }
knowledgeOptions.value = []
selectedKnowledge.value = []
}

function open(knowledge: KnowledgeSelection[]) {
resetData()
selectedKnowledge.value = cloneDeep(knowledge)
visible.value = true
}

function submit() {
emit('submit', cloneDeep(selectedKnowledge.value))
visible.value = false
}

defineExpose({ open })
</script>

<template>
<MkDialog v-model="visible" title="添加关联知识库" width="960" @closed="resetData">
<template #subtitle>所选知识库必须使用相同的 Embedding 模型</template>
<div class="flex h-105 gap-4">
<div class="w-55 shrink-0 border-r">
<FolderTree ref="folderTreeRef" :can-edit="false" :source="RESOURCE_TYPE.KNOWLEDGE" @loaded="selectFolder" @select="selectFolder" />
</div>
<div class="flex min-w-0 flex-1 flex-col gap-3">
<div class="flex-between gap-3">
<h6 class="truncate" :title="currentFolder.name">{{ currentFolder.name }}</h6>
<div class="flex items-center gap-2">
<MkSearchInput v-model="searchKeyword" placeholder="搜索知识库" @change="refreshKnowledge" />
<el-button text title="刷新" @click="refreshResources"><MkIcon :icon="Refresh" /></el-button>
</div>
</div>
<el-scrollbar class="flex-1">
<MkInfiniteScroll ref="infiniteScrollRef" v-model="knowledgeOptions" :load="loadKnowledgePage">
<div class="grid grid-cols-2 gap-2 pr-2">
<el-checkbox
v-for="knowledge in knowledgeOptions"
:key="knowledge.id"
:model-value="isSelected(knowledge.id)"
:disabled="isDisabled(knowledge)"
class="m-0! h-auto! min-w-0 rounded-md border p-3! [&_.el-checkbox__label]:min-w-0"
@change="toggleKnowledge(knowledge)"
>
<span class="flex min-w-0 items-center gap-2">
<KnowledgeIcon :type="knowledge.type" :size="24" class="shrink-0" />
<span class="min-w-0">
<span class="block truncate" :title="knowledge.name">{{ knowledge.name }}</span>
<span v-if="knowledge.desc" class="block truncate text-sm text-N600" :title="knowledge.desc">{{ knowledge.desc }}</span>
</span>
</span>
</el-checkbox>
</div>
<template #empty><MkEmpty :type="searchKeyword ? 'search' : 'default'" /></template>
</MkInfiniteScroll>
</el-scrollbar>
</div>
</div>
<template #footer>
<div class="flex-between">
<div class="flex items-center gap-2">
<span class="text-N600">已选择 {{ selectedKnowledge.length }} 项</span>
<el-button v-if="selectedKnowledge.length" link type="primary" @click="selectedKnowledge = []">清空</el-button>
</div>
<div>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="submit">确定</el-button>
</div>
</div>
</template>
</MkDialog>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import type { KnowledgeItem } from '@/api/types'

/** 已选知识库允许只保留 ID,避免旧工作流缺少快照时丢失关联。 */
export type KnowledgeSelection = Pick<KnowledgeItem, 'id'> & Partial<KnowledgeItem>
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { cloneDeep } from 'lodash'
import type { DynamicFormField } from '@/api/types'
import { MkDynamicsForm, type FormField } from '@/components/mk-dynamics-form'

defineOptions({ name: 'BaseNodeModelParamsDialog' })
defineOptions({ name: 'ModelParamsDialog' })

const emit = defineEmits<{ submit: [settings: Record<string, unknown>] }>()

Expand Down
Loading
Loading