diff --git a/ui/src/api/API_README.md b/ui/src/api/API_README.md index 877ae6054ab..c72d9f54e54 100644 --- a/ui/src/api/API_README.md +++ b/ui/src/api/API_README.md @@ -79,6 +79,23 @@ Action、Drawer 或 Dialog。复用方直接使用 `typeof XxxApi` 约束完整 额外维护逐方法接口,例如 `ModelActionApi`,也不要使用不断扩展的 `Pick`。 仅展示数据的组件不接收 API。 +### 知识库维护 + +`workspace/knowledge/knowledge.ts` 与 `workspace/shared.ts` 的 `getAllKnowledge(query)` +分别查询工作空间及共享知识库的非分页列表,返回 `KnowledgeItem[]`,用于关联知识库选择等 +需要全量选项的场景。原有 `getKnowledgePage` 继续用于分页列表。 + +`workspace/knowledge/knowledge.ts` 的 `putKnowledge` 更新普通知识库,`putLarkKnowledge` +更新飞书知识库;单项转移提交 `folder_id`。`putBatchMoveKnowledge` 将知识库 ID 数组和目标目录 +组装为 `{ id_list, folder_id }`,`putBatchDeleteKnowledge` 将 ID 数组组装为 `{ id_list }`, +分别使用 PUT 请求 `batch_move` 和 `batch_delete`。页面和 Action 负责类型判断及 loading。 + +### 知识库文档标签 + +`workspace/knowledge/knowledge.ts` 的 `getKnowledgeTags(knowledgeIds)` 查询所选知识库的全部文档 +标签,返回按标签名称分组的 `KnowledgeTagGroup[]`。工作流页面将查询方法提供给文档标签检索 +节点,节点负责本地搜索和最多 100 项的展示。 + ## 枚举与类型组织 API 枚举与类型统一在 `src/api` 范围内管理,相关规则由本文档统一维护。 diff --git a/ui/src/api/admin/workspace/knowledge/knowledge.ts b/ui/src/api/admin/workspace/knowledge/knowledge.ts index aaf3e3291f8..84f3c0acea7 100644 --- a/ui/src/api/admin/workspace/knowledge/knowledge.ts +++ b/ui/src/api/admin/workspace/knowledge/knowledge.ts @@ -1,4 +1,4 @@ -import { del, get } from '../../core/request' +import { del, get, post, put } from '../../core/request' import type { ParamsPage, ResponsePage } from '../../core/types' import type { Dict, KnowledgeItem } from '@/api/types' import { getWorkspaceId } from '@/utils/resource-context' @@ -8,6 +8,10 @@ const getPrefix = () => { return `/workspace/${workspaceId}/knowledge` } +/** 获取工作空间不分页的知识库列表。 */ +const getAllKnowledge = (query?: Dict) => { + return get(getPrefix(), query) +} /** 获取工作空间知识库分页列表。 */ const getKnowledgePage = (page: ParamsPage, query?: Dict) => { return get>(`${getPrefix()}/${page.currentPage}/${page.pageSize}`, query) @@ -18,4 +22,38 @@ const deleteKnowledge = (knowledgeId: string) => { return del(`${getPrefix()}/${knowledgeId}`) } -export default { deleteKnowledge, getKnowledgePage } +/** 更新工作空间知识库信息。 */ +const putKnowledge = (knowledgeId: string, payload: Partial) => { + return put, KnowledgeItem>(`${getPrefix()}/${knowledgeId}`, payload) +} + +/** 更新飞书知识库信息。 */ +const putLarkKnowledge = (knowledgeId: string, payload: Partial) => { + return put, KnowledgeItem>(`${getPrefix()}/lark/${knowledgeId}`, payload) +} + +/** 批量删除工作空间知识库。 */ +const putBatchDeleteKnowledge = (knowledgeIds: string[]) => { + return put<{ id_list: string[] }, boolean>(`${getPrefix()}/batch_delete`, { id_list: knowledgeIds }) +} + +/** 批量转移工作空间知识库。 */ +const putBatchMoveKnowledge = (knowledgeIds: string[], folderId: string) => { + return put<{ id_list: string[]; folder_id: string }, boolean>(`${getPrefix()}/batch_move`, { id_list: knowledgeIds, folder_id: folderId }) +} + +/** 导入知识库文件。 */ +const importKnowledgeBundle = (payload: FormData) => { + return post(`${getPrefix()}/import_knowledge`, payload) +} + +export default { + deleteKnowledge, + getAllKnowledge, + getKnowledgePage, + putKnowledge, + putLarkKnowledge, + putBatchDeleteKnowledge, + putBatchMoveKnowledge, + importKnowledgeBundle, +} diff --git a/ui/src/api/admin/workspace/shared.ts b/ui/src/api/admin/workspace/shared.ts index bbfa7ba9f02..46ef45e53eb 100644 --- a/ui/src/api/admin/workspace/shared.ts +++ b/ui/src/api/admin/workspace/shared.ts @@ -27,4 +27,9 @@ const getKnowledgePage = (page: ParamsPage, query?: Dict) => { return get>(`${getPrefix()}/knowledge/${page.currentPage}/${page.pageSize}`, query) } -export default { getKnowledgePage, getModelList, getToolPage, getAllTool } +/** 获取工作空间共享的不分页知识库列表。 */ +const getAllKnowledge = (query?: Dict) => { + return get(`${getPrefix()}/knowledge`, query) +} + +export default { getAllKnowledge, getKnowledgePage, getModelList, getToolPage, getAllTool } diff --git a/ui/src/api/enums/knowledge.ts b/ui/src/api/enums/knowledge.ts index 253949fb535..ba4c6d15307 100644 --- a/ui/src/api/enums/knowledge.ts +++ b/ui/src/api/enums/knowledge.ts @@ -1,5 +1,4 @@ /** 后端知识库类型。 */ -// TODO v3不再用数字表示 待修改 export const KNOWLEDGE_TYPE = { BASE: 0, WEB: 1, LARK: 2, YUQUE: 3, WORKFLOW: 4 } as const /** 知识库分段检索模式。 */ diff --git a/ui/src/api/types/knowledge.ts b/ui/src/api/types/knowledge.ts index 1f7d731f312..79413f6b7b7 100644 --- a/ui/src/api/types/knowledge.ts +++ b/ui/src/api/types/knowledge.ts @@ -25,3 +25,9 @@ export interface KnowledgeItem { user_id?: string | null workspace_id: string } + +/** 按标签名称分组的知识库标签。 */ +export interface KnowledgeTagGroup { + key: string + values: { id: string; value: string; create_time: string; update_time: string }[] +} diff --git a/ui/src/components/COMPONENT_README.md b/ui/src/components/COMPONENT_README.md index 83150c534b0..b814188a652 100644 --- a/ui/src/components/COMPONENT_README.md +++ b/ui/src/components/COMPONENT_README.md @@ -40,10 +40,9 @@ src/components/ │ │ ├── MoveToDialog.vue │ │ ├── VirtualizedTree.vue │ │ └── types.ts -│ ├── knowledge-selection-dialog/ -│ │ ├── index.vue # 关联知识库选择、文件夹与共享资源查询 -│ │ └── types.ts # 已选知识库快照类型 -│ ├── model-select/ +│ ├── select-knowledge-dialog/ +│ │ └── index.vue # 关联知识库选择、文件夹与共享资源查询 +│ ├── select-model/ │ │ ├── index.vue # 按供应商分组的模型选择器与可选参数按钮 │ │ └── ModelParamsDialog.vue # 模型参数动态表单弹窗 │ ├── workspace-dropdown/ @@ -145,7 +144,7 @@ import JsonInput from '@/components/codemirror-editor/Json.vue' import MkSourceCard from '@/components/mk-source-card/index.vue' import FolderTree from '@/components/business/folder-tree/index.vue' import MoveToDialog from '@/components/business/folder-tree/MoveToDialog.vue' -import ModelSelect from '@/components/business/model-select/index.vue' +import SelectModel from '@/components/business/select-model/index.vue' import WorkspaceDropdown from '@/components/business/workspace-dropdown/index.vue' import WorkspaceRelationTags from '@/components/business/workspace-relation-tags/index.vue' ``` @@ -282,6 +281,10 @@ Element Plus 使用 `ElOnlyChild` 处理浮层触发器。`el-tooltip`、`el-pop `mt-2 text-N600` 样式;仅使用 `subtitle` 时,组件仍会按照 Element Plus 原生的标题 ID 和样式类 渲染 `title`。内容区域超出最大高度后显示滚动条。 +Dialog 外壳仅在首次打开时挂载;默认在关闭动画结束、触发 `closed` 后整体卸载, +避免未打开的弹窗生成隐藏 DOM。设置 `destroy-on-close="false"` 时,首次打开后保留实例和内容。 +业务侧无需额外使用 `v-if="visible"`,以免跳过关闭动画及 `closed` 清理。 + ```vue @@ -514,6 +517,10 @@ Prop,未传入时读取当前路由的 `meta.title`;显式传入 `title=""` 智能体、知识库和工具的资源图标分别使用自动注册的 `ApplicationIcon`、`KnowledgeIcon` 和 `ToolIcon`。`ApplicationIcon` 在未传入 `icon` 时会回退到系统默认图标。 +`KnowledgeIcon` 的 `type` 接收接口数字类型 `KnowledgeType`,内部通过 +`constants/knowledge.ts` 的 `KNOWLEDGE_TYPE_MAP` 转换为字符串后选择图标。自定义 `icon` +优先;`BASE`、`YUQUE` 以及未提供或未知类型时沿用默认知识库图标。 + ```vue ``` @@ -826,6 +833,9 @@ function validateJson(rule: unknown, value: unknown, callback: (error?: Error) = ### MkSourceCard +`disabled` 默认为 `false`;传入 `true` 时不添加 `cursor-pointer`,并将卡片 `shadow` 从 +`hover` 改为 `always`。该属性仅控制卡片样式,不拦截点击或选择事件。 + 用于模型、工具等带来源信息的等高资源卡片。`title` 提供默认标题,`nick_name` 和 `create_time` 提供固定样式的创建信息;`subtitle` 插槽也始终应用 `text-sm text-N600`。默认插槽放置资源详情, `footer` 插槽作为左侧常驻内容并始终贴在卡片底部;无论是否传入内容,底部都会保留固定位置。 @@ -1098,12 +1108,12 @@ const formValue = ref>({}) 初始化、空配置回填和切回自定义赋值时保留一行空白选项。卡片单选仅在存在完整选项时显示默认值卡片区。 引用变量模式继续保留变量路径,不应用自定义选项过滤。 -动态表单的 Model 字段使用 `ModelSelect`,将 `attrs.provider_list` 中的模型快照映射为 +动态表单的 Model 字段使用 `SelectModel`,将 `attrs.provider_list` 中的模型快照映射为 扁平 `ModelItem[]`,由选择器统一分组和展示供应商。旧快照未保存状态时保留可选行为,已保存的 -状态按 `ModelSelect` 的可用性规则处理。切换模型一次性回写 `model_id` 和深拷贝后的 +状态按 `SelectModel` 的可用性规则处理。切换模型一次性回写 `model_id` 和深拷贝后的 `model_params_setting`;清空时回写空 ID 和空参数,避免修改原配置中的参数对象。 -Model 配置器的默认模型也使用 `ModelSelect`,仅展示已选的可选模型,不开启参数设置入口。 +Model 配置器的默认模型也使用 `SelectModel`,仅展示已选的可选模型,不开启参数设置入口。 选择时将模型 ID 转换为包含已配置参数的 `default_value` 对象,清空时重置为空对象。 ## 跨页面业务组件 @@ -1148,16 +1158,22 @@ import FolderTree from '@/components/business/folder-tree/index.vue' ``` -### KnowledgeSelectionDialog +### SelectKnowledgeDialog -关联知识库选择弹窗,手动导入 `@/components/business/knowledge-selection-dialog/index.vue`。 +关联知识库选择弹窗,手动导入 `@/components/business/select-knowledge-dialog/index.vue`。 通过 `open(knowledge)` 传入已选知识库快照,确认后通过 `submit` 返回新的选择;取消不修改调用方 -数据,关闭后统一清理临时状态。`KnowledgeSelection` 类型允许仅包含 ID,以兼容缺少详情的旧数据。 -组件复用只读 `FolderTree` 和 `MkInfiniteScroll` 查询工作空间及共享知识库,支持按名称搜索, -跨文件夹、搜索和分页保留选择,并限制新选知识库使用相同的 Embedding 模型。固定业务请求由 +数据,关闭后统一清理临时状态。选择数据直接基于 `KnowledgeItem` 声明为 +`(Partial & { id: string })[]`,保留必需的 ID 并兼容缺少详情的旧数据, +不再维护弹窗专用类型文件。 +组件采用 `MkViewLayout` 左右布局,搜索栏位于内容区右上角,刷新位于弹窗标题栏。 +选项使用紧凑三列布局(窄屏减少列数),左侧图标与省略名称、右侧复选框。点击卡片或复选框 +切换选择,选中后仅展示相同 Embedding 模型的选项,清空后恢复;悬停展示知识库详情。 +复用只读 `FolderTree`,通过工作空间及共享 API 的 `getAllKnowledge` 一次加载当前查询的全部 +知识库,不使用滚动分页。支持按名称搜索,跨文件夹和搜索保留选择,并限制新选知识库使用 +相同的 Embedding 模型。固定业务请求由 该组件负责,调用方维护最终关联 ID 和快照。 -### ModelSelect +### SelectModel 按供应商分组展示模型,通过 `v-model` 控制模型 ID,并在选择变化时触发 `change`。`options` 为 `ModelItem[]`,`providerOptions` 为 `ModelProviderItem[]`,模型列表和供应商列表均由使用方查询。 @@ -1180,10 +1196,10 @@ import FolderTree from '@/components/business/folder-tree/index.vue' ```vue - -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({ ...FOLDER_ENTRIES[RESOURCE_TYPE.KNOWLEDGE].all }) -const knowledgeOptions = ref([]) -const selectedKnowledge = ref([]) -const infiniteScrollRef = useTemplateRef<{ reset: () => Promise }>('infiniteScrollRef') -const folderTreeRef = useTemplateRef>('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 }) - - - diff --git a/ui/src/components/business/knowledge-selection-dialog/types.ts b/ui/src/components/business/knowledge-selection-dialog/types.ts deleted file mode 100644 index 2fb3231cf30..00000000000 --- a/ui/src/components/business/knowledge-selection-dialog/types.ts +++ /dev/null @@ -1,4 +0,0 @@ -import type { KnowledgeItem } from '@/api/types' - -/** 已选知识库允许只保留 ID,避免旧工作流缺少快照时丢失关联。 */ -export type KnowledgeSelection = Pick & Partial diff --git a/ui/src/components/business/select-knowledge-dialog/index.vue b/ui/src/components/business/select-knowledge-dialog/index.vue new file mode 100644 index 00000000000..7239a6f95f6 --- /dev/null +++ b/ui/src/components/business/select-knowledge-dialog/index.vue @@ -0,0 +1,184 @@ + + + diff --git a/ui/src/components/business/model-select/ModelParamsDialog.vue b/ui/src/components/business/select-model/ModelParamsDialog.vue similarity index 100% rename from ui/src/components/business/model-select/ModelParamsDialog.vue rename to ui/src/components/business/select-model/ModelParamsDialog.vue diff --git a/ui/src/components/business/model-select/index.vue b/ui/src/components/business/select-model/index.vue similarity index 94% rename from ui/src/components/business/model-select/index.vue rename to ui/src/components/business/select-model/index.vue index 19e429c582f..12b686f02b8 100644 --- a/ui/src/components/business/model-select/index.vue +++ b/ui/src/components/business/select-model/index.vue @@ -9,7 +9,7 @@ import ModelCreateButton from '@/views/model/create-model/ModelCreateButton.vue' import ModelApi from '@/api/admin/workspace/model/model' import SystemSharedModelApi from '@/api/admin/system/shared-resources/model' import { isWorkspaceResource, isSystemSharedResource } from '@/utils/resource-context' -defineOptions({ name: 'ModelSelect', inheritAttrs: false }) +defineOptions({ name: 'SelectModel', inheritAttrs: false }) interface ModelOptionGroup { icon: string @@ -18,11 +18,11 @@ interface ModelOptionGroup { provider: string } -type ModelSelectValue = Multiple extends true ? string[] : string +type SelectModelValue = Multiple extends true ? string[] : string const props = withDefaults( defineProps<{ - modelValue: ModelSelectValue + modelValue: SelectModelValue options: ModelItem[] providerOptions: ModelProviderItem[] canEditParams?: boolean @@ -42,9 +42,9 @@ const props = withDefaults( ) const emit = defineEmits<{ - change: [modelValue: ModelSelectValue] + change: [modelValue: SelectModelValue] refresh: [] - 'update:modelValue': [modelValue: ModelSelectValue] + 'update:modelValue': [modelValue: SelectModelValue] 'update:modelParams': [settings: Record] }>() @@ -69,7 +69,7 @@ const _options = computed(() => { const loading = ref(false) const canEditModelParams = computed(() => props.canEditParams && !props.multiple) -const selectedModelValue = computed({ +const selectedModelValue = computed({ get: () => props.modelValue, set: (modelValue) => { emit('update:modelValue', modelValue) @@ -117,7 +117,7 @@ function openModelParams() { diff --git a/ui/src/workflow-canvas/nodes/tool-base-node/component/output/OutputFieldFormDialog.vue b/ui/src/workflow-canvas/nodes/tool-base-node/component/output/OutputFieldFormDialog.vue index ae72f4e6986..a279da6dff5 100644 --- a/ui/src/workflow-canvas/nodes/tool-base-node/component/output/OutputFieldFormDialog.vue +++ b/ui/src/workflow-canvas/nodes/tool-base-node/component/output/OutputFieldFormDialog.vue @@ -64,7 +64,7 @@ defineExpose({ open, close }) \ No newline at end of file + + diff --git a/ui/src/workflow-canvas/nodes/tool-base-node/component/output/OutputTitleDialog.vue b/ui/src/workflow-canvas/nodes/tool-base-node/component/output/OutputTitleDialog.vue index 2f516d281f8..8f17024114d 100644 --- a/ui/src/workflow-canvas/nodes/tool-base-node/component/output/OutputTitleDialog.vue +++ b/ui/src/workflow-canvas/nodes/tool-base-node/component/output/OutputTitleDialog.vue @@ -12,9 +12,7 @@ const form = ref({ }) const rules = reactive({ - title: [ - { required: true, message: '请输入标题', trigger: 'blur' }, - ], + title: [{ required: true, message: '请输入标题', trigger: 'blur' }], }) const dialogVisible = ref(false) @@ -43,7 +41,7 @@ defineExpose({ open, close }) \ No newline at end of file + + diff --git a/ui/src/workflow-canvas/nodes/variable-aggregation-node/index.vue b/ui/src/workflow-canvas/nodes/variable-aggregation-node/index.vue index d3ceb34dce1..678d36fefc1 100644 --- a/ui/src/workflow-canvas/nodes/variable-aggregation-node/index.vue +++ b/ui/src/workflow-canvas/nodes/variable-aggregation-node/index.vue @@ -161,7 +161,7 @@ onBeforeUnmount(() => {