Commit 661cda83 by light5980 Committed by GitHub

Deleted show (#7043)

* feat(app): add knowledge base deleted show

# Conflicts:
#	projects/app/test/service/core/app/rewriteAppWorkflowToDetail.test.ts

* feat: workflow konwledge base hover

* perf: rewrite app detail

* udpate doc

* fix: review

* fix: test

---------

Co-authored-by: archer <545436317@qq.com>
parent ab0b7456
...@@ -280,7 +280,7 @@ ChatItem ...@@ -280,7 +280,7 @@ ChatItem
设计文档: 设计文档:
- `.agent/design/bug/stream-resume-form-input-file-list.md` - `.agents/design/bug/stream-resume-form-input-file-list.md`
- 新增本次问题的设计说明、根因、方案、测试和 TODO。 - 新增本次问题的设计说明、根因、方案、测试和 TODO。
### 数据合并与恢复工具 ### 数据合并与恢复工具
......
...@@ -10,7 +10,7 @@ FastGPT 是一个 AI Agent 构建平台,通过 Flow 提供开箱即用的数据 ...@@ -10,7 +10,7 @@ FastGPT 是一个 AI Agent 构建平台,通过 Flow 提供开箱即用的数据
## 设计文档 ## 设计文档
你可以参考 [项目设计文档](./.agent/design/) 来了解 FastGPT 已有的设计方案。 你可以参考 [项目设计文档](./.agents/design/) 来了解 FastGPT 已有的设计方案。
## 架构 ## 架构
...@@ -34,7 +34,7 @@ FastGPT 是一个 AI Agent 构建平台,通过 Flow 提供开箱即用的数据 ...@@ -34,7 +34,7 @@ FastGPT 是一个 AI Agent 构建平台,通过 Flow 提供开箱即用的数据
## 开发命令 ## 开发命令
常用开发命令见 [FastGPT 开发命令](./.agent/code/commands.md) 常用开发命令见 [FastGPT 开发命令](./.agents/code/commands.md)
## 测试 ## 测试
...@@ -87,7 +87,7 @@ FastGPT 是一个 AI Agent 构建平台,通过 Flow 提供开箱即用的数据 ...@@ -87,7 +87,7 @@ FastGPT 是一个 AI Agent 构建平台,通过 Flow 提供开箱即用的数据
## 代码规范 ## 代码规范
[FastGPT 代码规范](./.agent/code/syntax.md) [FastGPT 代码规范](./.agents/code/syntax.md)
### API 入参校验 ### API 入参校验
...@@ -105,7 +105,7 @@ const { body, query } = parseApiInput({ ...@@ -105,7 +105,7 @@ const { body, query } = parseApiInput({
``` ```
- 这个 helper 只用于 API 边界的请求入参校验。内部业务数据、数据库记录、模型返回、工具调用参数等 schema 校验仍使用普通 `Schema.parse(...)`,因为这些错误应按内部 bug 上报。 - 这个 helper 只用于 API 边界的请求入参校验。内部业务数据、数据库记录、模型返回、工具调用参数等 schema 校验仍使用普通 `Schema.parse(...)`,因为这些错误应按内部 bug 上报。
- 相关设计见 [Zod 请求入参错误降噪设计](./.agent/design/api/zod-request-parse-error-handling.md) - 相关设计见 [Zod 请求入参错误降噪设计](./.agents/design/api/zod-request-parse-error-handling.md)
### 函数注释 ### 函数注释
...@@ -165,8 +165,8 @@ function agent_loop(用户需求){ ...@@ -165,8 +165,8 @@ function agent_loop(用户需求){
1. 输出语言:中文 1. 输出语言:中文
2. 输出文档位置: 2. 输出文档位置:
2.1. 设计文档: [.agent/design](.agent/design),todo 跟在设计文档后面。 2.1. 设计文档: [.agents/design](.agents/design),todo 跟在设计文档后面。
2.2. 问题分析文档: [.agent/issue](.agent/issue) 2.2. 问题分析文档: [.agents/issue](.agents/issue)
3. 相同需求文档,尽量写在一起(内容超过 300 行,可以分批写入),或者创建要给目录一起管理,不要随意平铺一堆不同版本的相同问题的文档。 3. 相同需求文档,尽量写在一起(内容超过 300 行,可以分批写入),或者创建要给目录一起管理,不要随意平铺一堆不同版本的相同问题的文档。
4. 文件输出,使用正确的编码格式,例如UTF-8。 4. 文件输出,使用正确的编码格式,例如UTF-8。
5. 除非用户指明,否则不要编写总结报告。 5. 除非用户指明,否则不要编写总结报告。
...@@ -48,6 +48,7 @@ fastgpt-plugin: ...@@ -48,6 +48,7 @@ fastgpt-plugin:
3. 插件运行入口支持从对象存储拉取,并缓存到本地文件目录。 3. 插件运行入口支持从对象存储拉取,并缓存到本地文件目录。
4. 输入引导配置增加校验,避免错误配置了自定义词库地址。 4. 输入引导配置增加校验,避免错误配置了自定义词库地址。
5. 工作流数组引用类型增强校验,避免刚好与二维数据冲突。 5. 工作流数组引用类型增强校验,避免刚好与二维数据冲突。
6. 知识库被删除后,应用编排时优雅提示。
## 🐛 修复 ## 🐛 修复
......
...@@ -275,7 +275,7 @@ ...@@ -275,7 +275,7 @@
"content/self-host/upgrading/4-15/41503.en.mdx": "2026-05-28T16:21:09+08:00", "content/self-host/upgrading/4-15/41503.en.mdx": "2026-05-28T16:21:09+08:00",
"content/self-host/upgrading/4-15/41503.mdx": "2026-05-28T16:21:09+08:00", "content/self-host/upgrading/4-15/41503.mdx": "2026-05-28T16:21:09+08:00",
"content/self-host/upgrading/4-15/41504.en.mdx": "2026-06-01T17:19:55+08:00", "content/self-host/upgrading/4-15/41504.en.mdx": "2026-06-01T17:19:55+08:00",
"content/self-host/upgrading/4-15/41504.mdx": "2026-06-04T16:10:15+08:00", "content/self-host/upgrading/4-15/41504.mdx": "2026-06-05T13:29:50+08:00",
"content/self-host/upgrading/outdated/40.en.mdx": "2026-04-26T21:08:47+08:00", "content/self-host/upgrading/outdated/40.en.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/outdated/40.mdx": "2026-04-26T21:08:47+08:00", "content/self-host/upgrading/outdated/40.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/outdated/41.en.mdx": "2026-04-26T21:08:47+08:00", "content/self-host/upgrading/outdated/41.en.mdx": "2026-04-26T21:08:47+08:00",
......
...@@ -20,7 +20,8 @@ export const SelectedDatasetSchema = z.object({ ...@@ -20,7 +20,8 @@ export const SelectedDatasetSchema = z.object({
model: z.string().meta({ model: z.string().meta({
description: '知识库使用的向量模型' description: '知识库使用的向量模型'
}) })
}) }),
isDeleted: BoolSchema.optional()
}); });
export type SelectedDatasetType = z.infer<typeof SelectedDatasetSchema>; export type SelectedDatasetType = z.infer<typeof SelectedDatasetSchema>;
......
...@@ -60,9 +60,10 @@ export const getHandleId = ( ...@@ -60,9 +60,10 @@ export const getHandleId = (
return `${nodeId}-${type}-${key}`; return `${nodeId}-${type}-${key}`;
}; };
export const checkInputIsReference = (input: FlowNodeInputItemType) => { export const nodeInputIsReference = (input: FlowNodeInputItemType) => {
if (input.renderTypeList?.[input?.selectedTypeIndex || 0] === FlowNodeInputTypeEnum.reference) if (input.renderTypeList?.[input?.selectedTypeIndex || 0] === FlowNodeInputTypeEnum.reference) {
return true; return true;
}
return false; return false;
}; };
...@@ -410,7 +411,9 @@ export const formatEditorVariablePickerIcon = ( ...@@ -410,7 +411,9 @@ export const formatEditorVariablePickerIcon = (
// Check the value is a valid reference value format: [variableId, outputId] // Check the value is a valid reference value format: [variableId, outputId]
export const isValidReferenceValueFormat = ( export const isValidReferenceValueFormat = (
value: any, value: any,
nodesMap?: Record<string, Pick<StoreNodeItemType, 'nodeId'>> | Map<string, Pick<StoreNodeItemType, 'nodeId'>> nodesMap?:
| Record<string, Pick<StoreNodeItemType, 'nodeId'>>
| Map<string, Pick<StoreNodeItemType, 'nodeId'>>
): value is ReferenceItemValueType => { ): value is ReferenceItemValueType => {
if (!(Array.isArray(value) && value.length === 2 && typeof value[0] === 'string')) { if (!(Array.isArray(value) && value.length === 2 && typeof value[0] === 'string')) {
return false; return false;
......
import { describe, expect, it, vi, beforeEach } from 'vitest'; import { describe, expect, it, vi, beforeEach } from 'vitest';
import { import {
getHandleId, getHandleId,
checkInputIsReference, nodeInputIsReference,
getGuideModule, getGuideModule,
splitGuideModule, splitGuideModule,
getAppChatConfig, getAppChatConfig,
...@@ -65,14 +65,14 @@ describe('getHandleId', () => { ...@@ -65,14 +65,14 @@ describe('getHandleId', () => {
}); });
}); });
describe('checkInputIsReference', () => { describe('nodeInputIsReference', () => {
it('should return true when renderTypeList first item is reference', () => { it('should return true when renderTypeList first item is reference', () => {
const input: FlowNodeInputItemType = { const input: FlowNodeInputItemType = {
key: 'test', key: 'test',
label: 'Test', label: 'Test',
renderTypeList: [FlowNodeInputTypeEnum.reference] renderTypeList: [FlowNodeInputTypeEnum.reference]
}; };
expect(checkInputIsReference(input)).toBe(true); expect(nodeInputIsReference(input)).toBe(true);
}); });
it('should return true when selectedTypeIndex points to reference', () => { it('should return true when selectedTypeIndex points to reference', () => {
...@@ -82,7 +82,7 @@ describe('checkInputIsReference', () => { ...@@ -82,7 +82,7 @@ describe('checkInputIsReference', () => {
renderTypeList: [FlowNodeInputTypeEnum.input, FlowNodeInputTypeEnum.reference], renderTypeList: [FlowNodeInputTypeEnum.input, FlowNodeInputTypeEnum.reference],
selectedTypeIndex: 1 selectedTypeIndex: 1
}; };
expect(checkInputIsReference(input)).toBe(true); expect(nodeInputIsReference(input)).toBe(true);
}); });
it('should return false when renderTypeList first item is not reference', () => { it('should return false when renderTypeList first item is not reference', () => {
...@@ -91,7 +91,7 @@ describe('checkInputIsReference', () => { ...@@ -91,7 +91,7 @@ describe('checkInputIsReference', () => {
label: 'Test', label: 'Test',
renderTypeList: [FlowNodeInputTypeEnum.input] renderTypeList: [FlowNodeInputTypeEnum.input]
}; };
expect(checkInputIsReference(input)).toBe(false); expect(nodeInputIsReference(input)).toBe(false);
}); });
it('should return false when selectedTypeIndex is 0 and first item is not reference', () => { it('should return false when selectedTypeIndex is 0 and first item is not reference', () => {
...@@ -101,7 +101,7 @@ describe('checkInputIsReference', () => { ...@@ -101,7 +101,7 @@ describe('checkInputIsReference', () => {
renderTypeList: [FlowNodeInputTypeEnum.input, FlowNodeInputTypeEnum.reference], renderTypeList: [FlowNodeInputTypeEnum.input, FlowNodeInputTypeEnum.reference],
selectedTypeIndex: 0 selectedTypeIndex: 0
}; };
expect(checkInputIsReference(input)).toBe(false); expect(nodeInputIsReference(input)).toBe(false);
}); });
it('should return false when renderTypeList is undefined', () => { it('should return false when renderTypeList is undefined', () => {
...@@ -109,7 +109,7 @@ describe('checkInputIsReference', () => { ...@@ -109,7 +109,7 @@ describe('checkInputIsReference', () => {
key: 'test', key: 'test',
label: 'Test' label: 'Test'
} as FlowNodeInputItemType; } as FlowNodeInputItemType;
expect(checkInputIsReference(input)).toBe(false); expect(nodeInputIsReference(input)).toBe(false);
}); });
it('should use index 0 when selectedTypeIndex is undefined', () => { it('should use index 0 when selectedTypeIndex is undefined', () => {
...@@ -118,7 +118,7 @@ describe('checkInputIsReference', () => { ...@@ -118,7 +118,7 @@ describe('checkInputIsReference', () => {
label: 'Test', label: 'Test',
renderTypeList: [FlowNodeInputTypeEnum.reference, FlowNodeInputTypeEnum.input] renderTypeList: [FlowNodeInputTypeEnum.reference, FlowNodeInputTypeEnum.input]
}; };
expect(checkInputIsReference(input)).toBe(true); expect(nodeInputIsReference(input)).toBe(true);
}); });
}); });
......
...@@ -33,19 +33,57 @@ import { mongoSessionRun } from '../../common/mongo/sessionRun'; ...@@ -33,19 +33,57 @@ import { mongoSessionRun } from '../../common/mongo/sessionRun';
import { getLogger, LogCategories } from '../../common/logger'; import { getLogger, LogCategories } from '../../common/logger';
import { deleteSandboxesByAppId, deleteSandboxesByChatIds } from '../ai/sandbox/service/resource'; import { deleteSandboxesByAppId, deleteSandboxesByChatIds } from '../ai/sandbox/service/resource';
import { MongoSystemTool } from '../plugin/tool/systemToolSchema'; import { MongoSystemTool } from '../plugin/tool/systemToolSchema';
import {
SelectedAgentSkillItemTypeSchema,
type AppFormEditFormType
} from '@fastgpt/global/core/app/formEdit/type';
import z from 'zod';
import { nodeInputIsReference } from '@fastgpt/global/core/workflow/utils';
const logger = getLogger(LogCategories.MODULE.APP.FOLDER); const logger = getLogger(LogCategories.MODULE.APP.FOLDER);
/**
* 在更新应用前,对工作流节点数据进行格式化和安全处理。
* 主要职责:
* 1. 知识库:统一数据结构为 { datasetId: string }[]。
* 2. Skill: 统一数据结构为 { skillId: string }[]。
* 2. 敏感信息(如 Header Secret、密码类型输入、系统工具手动配置的密钥)进行加密存储。
*/
export const beforeUpdateAppFormat = ({ nodes }: { nodes?: StoreNodeItemType[] }) => { export const beforeUpdateAppFormat = ({ nodes }: { nodes?: StoreNodeItemType[] }) => {
if (!nodes) return; if (!nodes) return;
const StoredSelectedDatasetSchema = z.object({
datasetId: z.string()
});
/**
* 格式化数据集选择值,保存阶段只保留 datasetId,移除编辑态快照字段。
* 引用模式由调用处判断并跳过,避免把 [nodeId, key] 误压缩成空数组。
* 兼容历史单选格式 { datasetId },避免旧应用再次保存时丢失知识库配置。
*/
const formatDatasetSelectValue = (value: unknown) => {
const datasets = z
.union([StoredSelectedDatasetSchema, z.array(StoredSelectedDatasetSchema)])
.parse(value);
const datasetList = Array.isArray(datasets) ? datasets : [datasets];
return datasetList.map(({ datasetId }) => ({ datasetId }));
};
nodes.forEach((node) => { nodes.forEach((node) => {
const isDatasetNode =
node.flowNodeType === FlowNodeTypeEnum.datasetSearchNode ||
node.flowNodeType === FlowNodeTypeEnum.agent;
// Format header secret // Format header secret
node.inputs.forEach((input) => { node.inputs.forEach((input) => {
if (nodeInputIsReference(input)) return;
// 敏感信息
if (input.key === NodeInputKeyEnum.headerSecret && typeof input.value === 'object') { if (input.key === NodeInputKeyEnum.headerSecret && typeof input.value === 'object') {
input.value = storeSecretValue(input.value); input.value = storeSecretValue(input.value);
} }
if (input.renderTypeList.includes(FlowNodeInputTypeEnum.password)) { if (input.renderTypeList?.includes(FlowNodeInputTypeEnum.password)) {
input.value = encryptSecretValue(input.value); input.value = encryptSecretValue(input.value);
} }
if (input.key === NodeInputKeyEnum.systemInputConfig && typeof input.value === 'object') { if (input.key === NodeInputKeyEnum.systemInputConfig && typeof input.value === 'object') {
...@@ -59,35 +97,31 @@ export const beforeUpdateAppFormat = ({ nodes }: { nodes?: StoreNodeItemType[] } ...@@ -59,35 +97,31 @@ export const beforeUpdateAppFormat = ({ nodes }: { nodes?: StoreNodeItemType[] }
} }
}); });
} }
});
// Format dataset search // 知识库
if (node.flowNodeType === FlowNodeTypeEnum.datasetSearchNode) { if (isDatasetNode) {
node.inputs.forEach((input) => { // Agent
if (input.key === NodeInputKeyEnum.datasetSelectList) { if (input.key === NodeInputKeyEnum.datasetSelectList) {
const val = input.value as undefined | { datasetId: string }[] | { datasetId: string }; input.value = formatDatasetSelectValue(input.value);
if (!val) { }
input.value = []; // workflow
} else if (Array.isArray(val)) { if (input.key === NodeInputKeyEnum.datasetParams) {
// Not rewrite reference value const datasetParams = input.value as AppFormEditFormType['dataset'] | undefined;
if (val.length === 2 && val.every((item) => typeof item === 'string')) { if (datasetParams?.datasets) {
return; input.value = {
} ...datasetParams,
input.value = val datasets: formatDatasetSelectValue(datasetParams.datasets)
.map((dataset: { datasetId: string }) => ({ };
datasetId: dataset.datasetId
}))
.filter((item) => !!item.datasetId);
} else if (typeof val === 'object' && val !== null) {
input.value = [
{
datasetId: val.datasetId
}
];
} }
} }
}); }
}
// Skills
if (input.key === NodeInputKeyEnum.skills) {
const skills = z.array(SelectedAgentSkillItemTypeSchema).parse(input.value);
input.value = skills.map(({ isDeleted, description, avatar, ...skill }) => skill);
}
});
}); });
}; };
......
...@@ -1162,6 +1162,7 @@ ...@@ -1162,6 +1162,7 @@
"test_model_tip": "This model is a test model and does not support high concurrency use.", "test_model_tip": "This model is a test model and does not support high concurrency use.",
"textarea_variable_picker_tip": "Enter \"/\" to select a variable", "textarea_variable_picker_tip": "Enter \"/\" to select a variable",
"to_dataset": "To dataset", "to_dataset": "To dataset",
"dataset_deleted": "Dataset has been deleted",
"tool_invalid": "Tool has expired", "tool_invalid": "Tool has expired",
"tool_invalid_click_delete_tip": "Click delete", "tool_invalid_click_delete_tip": "Click delete",
"total_num": "Total: {{num}}", "total_num": "Total: {{num}}",
......
...@@ -1163,6 +1163,7 @@ ...@@ -1163,6 +1163,7 @@
"test_model_tip": "该模型为测试模型,不支持高并发使用。", "test_model_tip": "该模型为测试模型,不支持高并发使用。",
"textarea_variable_picker_tip": "输入\"/\"可选择变量", "textarea_variable_picker_tip": "输入\"/\"可选择变量",
"to_dataset": "前往知识库", "to_dataset": "前往知识库",
"dataset_deleted": "知识库已被删除",
"tool_invalid": "工具已失效", "tool_invalid": "工具已失效",
"tool_invalid_click_delete_tip": "点击删除", "tool_invalid_click_delete_tip": "点击删除",
"total_num": "总数: {{num}}", "total_num": "总数: {{num}}",
......
...@@ -1152,6 +1152,7 @@ ...@@ -1152,6 +1152,7 @@
"test_model_tip": "此模型為測試模型,不支援高並發使用。", "test_model_tip": "此模型為測試模型,不支援高並發使用。",
"textarea_variable_picker_tip": "輸入「/」以選擇變數", "textarea_variable_picker_tip": "輸入「/」以選擇變數",
"to_dataset": "前往知識庫", "to_dataset": "前往知識庫",
"dataset_deleted": "知識庫已被刪除",
"tool_invalid": "工具已失效", "tool_invalid": "工具已失效",
"tool_invalid_click_delete_tip": "點擊刪除", "tool_invalid_click_delete_tip": "點擊刪除",
"total_num": "總數: {{num}}", "total_num": "總數: {{num}}",
......
import React from 'react';
import { Box, Flex, type FlexProps } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyIconButton, { MyDeleteIconButton } from '@fastgpt/web/components/common/Icon/button';
import type { SelectedDatasetType } from '@fastgpt/global/core/workflow/type/io';
type DatasetCardProps = {
dataset: SelectedDatasetType;
onDelete?: (datasetId: string) => void;
flexProps?: FlexProps;
};
const formCardShadow = '0 4px 8px -2px rgba(16,24,40,.1),0 2px 4px -2px rgba(16,24,40,.06)';
const cardProps: FlexProps = {
w: '100%',
minW: 0,
maxW: '100%',
p: 2,
bg: 'white',
boxShadow: formCardShadow,
borderRadius: 'md',
border: 'base'
};
/**
* 单个已选知识库卡片,仅消费后端补齐的 isDeleted 状态来展示正常态或删除态。
*/
const DatasetCard = React.memo(function DatasetCard({
dataset,
onDelete,
flexProps
}: DatasetCardProps) {
const { t } = useTranslation();
const router = useRouter();
const isDeleted = !!dataset.isDeleted;
const hasPreviewButton = !isDeleted;
const hasDeleteButton = !!onDelete;
const hasController = hasPreviewButton || hasDeleteButton;
return (
<Flex
overflow={'hidden'}
alignItems={'center'}
userSelect={'none'}
{...cardProps}
{...flexProps}
border={flexProps?.border || cardProps.border}
borderColor={isDeleted ? 'red.600' : flexProps?.borderColor}
_hover={{
...flexProps?._hover,
borderColor: isDeleted ? 'red.600' : 'primary.300',
'& .dataset-card-controller': {
opacity: 1,
pointerEvents: 'auto'
}
}}
>
<Avatar src={dataset.avatar} w={'1.5rem'} borderRadius={'sm'} />
<Box
ml={2}
flex={'1 1 auto'}
w={0}
minW={0}
className={'textEllipsis'}
fontSize={'sm'}
color={isDeleted ? 'red.600' : 'myGray.900'}
>
{isDeleted ? t('common:dataset_deleted') : dataset.name}
</Box>
{hasController && (
<Box
className="dataset-card-controller"
ml={2}
flexShrink={0}
display={'flex'}
alignItems={'center'}
opacity={[1, 0]}
pointerEvents={['auto', 'none']}
>
{hasPreviewButton && (
<MyIconButton
icon={'common/viewLight'}
onClick={(e) => {
e.stopPropagation();
router.push({
pathname: '/dataset/detail',
query: {
datasetId: dataset.datasetId
}
});
}}
/>
)}
{hasDeleteButton && (
<MyDeleteIconButton
onClick={(e) => {
e.stopPropagation();
onDelete?.(dataset.datasetId);
}}
/>
)}
</Box>
)}
</Flex>
);
});
export default DatasetCard;
...@@ -9,9 +9,9 @@ import { ...@@ -9,9 +9,9 @@ import {
Checkbox, Checkbox,
VStack, VStack,
HStack, HStack,
IconButton,
Spacer, Spacer,
useDisclosure useDisclosure,
IconButton
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { ChevronRightIcon, CloseIcon } from '@chakra-ui/icons'; import { ChevronRightIcon, CloseIcon } from '@chakra-ui/icons';
import Avatar from '@fastgpt/web/components/common/Avatar'; import Avatar from '@fastgpt/web/components/common/Avatar';
...@@ -45,6 +45,12 @@ export const DatasetSelectModal = ({ ...@@ -45,6 +45,12 @@ export const DatasetSelectModal = ({
// Current selected datasets, initialized with defaultSelectedDatasets // Current selected datasets, initialized with defaultSelectedDatasets
const [selectedDatasets, setSelectedDatasets] = const [selectedDatasets, setSelectedDatasets] =
useState<SelectedDatasetType[]>(defaultSelectedDatasets); useState<SelectedDatasetType[]>(defaultSelectedDatasets);
// 已删除知识库只在弹窗确认时被写回移除;关闭弹窗不影响外部配置。
const availableSelectedDatasets = useMemo(
() => selectedDatasets.filter((dataset) => !dataset.isDeleted),
[selectedDatasets]
);
const hasDeletedSelectedDatasets = availableSelectedDatasets.length !== selectedDatasets.length;
const { toast } = useToast(); const { toast } = useToast();
const { userInfo } = useUserStore(); const { userInfo } = useUserStore();
...@@ -61,14 +67,14 @@ export const DatasetSelectModal = ({ ...@@ -61,14 +67,14 @@ export const DatasetSelectModal = ({
} = useDatasetSelect(); } = useDatasetSelect();
// The vector model of the first selected dataset // The vector model of the first selected dataset
const activeVectorModel = selectedDatasets[0]?.vectorModel?.model; const activeVectorModel = availableSelectedDatasets[0]?.vectorModel?.model;
// Check if a dataset is selected // Check if a dataset is selected
const isDatasetSelected = useCallback( const isDatasetSelected = useCallback(
(datasetId: string) => { (datasetId: string) => {
return selectedDatasets.some((dataset) => dataset.datasetId === datasetId); return availableSelectedDatasets.some((dataset) => dataset.datasetId === datasetId);
}, },
[selectedDatasets] [availableSelectedDatasets]
); );
// Check if a dataset is disabled (vector model mismatch) // Check if a dataset is disabled (vector model mismatch)
...@@ -98,11 +104,13 @@ export const DatasetSelectModal = ({ ...@@ -98,11 +104,13 @@ export const DatasetSelectModal = ({
return false; return false;
} }
const selectedDatasetIds = new Set(selectedDatasets.map((dataset) => dataset.datasetId)); const selectedDatasetIds = new Set(
availableSelectedDatasets.map((dataset) => dataset.datasetId)
);
return compatibleDatasetsByModel.every((item: DatasetListItemType) => return compatibleDatasetsByModel.every((item: DatasetListItemType) =>
selectedDatasetIds.has(item._id) selectedDatasetIds.has(item._id)
); );
}, [compatibleDatasetsByModel, selectedDatasets]); }, [availableSelectedDatasets, compatibleDatasetsByModel]);
const onSelect = (item: DatasetListItemType, checked: boolean) => { const onSelect = (item: DatasetListItemType, checked: boolean) => {
if (checked) { if (checked) {
...@@ -118,7 +126,8 @@ export const DatasetSelectModal = ({ ...@@ -118,7 +126,8 @@ export const DatasetSelectModal = ({
datasetId: item._id, datasetId: item._id,
avatar: item.avatar, avatar: item.avatar,
name: item.name, name: item.name,
vectorModel: item.vectorModel vectorModel: item.vectorModel,
isDeleted: false
} }
]); ]);
} else { } else {
...@@ -343,7 +352,8 @@ export const DatasetSelectModal = ({ ...@@ -343,7 +352,8 @@ export const DatasetSelectModal = ({
datasetId: item._id, datasetId: item._id,
avatar: item.avatar, avatar: item.avatar,
name: item.name, name: item.name,
vectorModel: item.vectorModel vectorModel: item.vectorModel,
isDeleted: false
}) })
); );
setSelectedDatasets((prev) => [...prev, ...newSelections]); setSelectedDatasets((prev) => [...prev, ...newSelections]);
...@@ -373,7 +383,7 @@ export const DatasetSelectModal = ({ ...@@ -373,7 +383,7 @@ export const DatasetSelectModal = ({
<> <>
{/* Selected count display */} {/* Selected count display */}
<Box mb={3} px={4} fontSize="sm" color="myGray.600"> <Box mb={3} px={4} fontSize="sm" color="myGray.600">
{t('app:Selected')}: {selectedDatasets.length} {t('app:dataset')} {t('app:Selected')}: {availableSelectedDatasets.length} {t('app:dataset')}
</Box> </Box>
{/* Selected dataset list */} {/* Selected dataset list */}
<VStack <VStack
...@@ -385,10 +395,10 @@ export const DatasetSelectModal = ({ ...@@ -385,10 +395,10 @@ export const DatasetSelectModal = ({
h={0} h={0}
minH={0} minH={0}
> >
{selectedDatasets.length === 0 && !isFetching && ( {availableSelectedDatasets.length === 0 && !isFetching && (
<EmptyTip text={t('app:No_selected_dataset')} /> <EmptyTip text={t('app:No_selected_dataset')} />
)} )}
{selectedDatasets.map((item) => ( {availableSelectedDatasets.map((item) => (
<Flex <Flex
key={item.datasetId} key={item.datasetId}
px={2} px={2}
...@@ -441,7 +451,7 @@ export const DatasetSelectModal = ({ ...@@ -441,7 +451,7 @@ export const DatasetSelectModal = ({
</Button> </Button>
)} )}
<Spacer /> <Spacer />
{isRootEmpty ? ( {isRootEmpty && !hasDeletedSelectedDatasets ? (
<Button <Button
px={3.5} px={3.5}
maxH={8} maxH={8}
...@@ -455,19 +465,21 @@ export const DatasetSelectModal = ({ ...@@ -455,19 +465,21 @@ export const DatasetSelectModal = ({
</Button> </Button>
) : ( ) : (
<HStack spacing={3} align="center"> <HStack spacing={3} align="center">
<Flex {!isRootEmpty && (
px={3} <Flex
py={1.5} px={3}
borderRadius={'sm'} py={1.5}
bg={'primary.50'} borderRadius={'sm'}
alignItems={'center'} bg={'primary.50'}
fontSize={'11px'} alignItems={'center'}
color={'primary.600'} fontSize={'11px'}
gap={1} color={'primary.600'}
> gap={1}
<MyIcon name={'common/info'} w={3.5} /> >
{t('app:dataset.Select_dataset_model_tip')} <MyIcon name={'common/info'} w={3.5} />
</Flex> {t('app:dataset.Select_dataset_model_tip')}
</Flex>
)}
<Button <Button
px={3.5} px={3.5}
maxH={8} maxH={8}
...@@ -475,7 +487,7 @@ export const DatasetSelectModal = ({ ...@@ -475,7 +487,7 @@ export const DatasetSelectModal = ({
onClick={() => { onClick={() => {
// Close modal and return selected datasets // Close modal and return selected datasets
onClose(); onClose();
onChange(selectedDatasets); onChange(availableSelectedDatasets);
}} }}
> >
{t('common:Confirm')} {t('common:Confirm')}
......
...@@ -10,7 +10,6 @@ import { ...@@ -10,7 +10,6 @@ import {
Switch Switch
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import type { AppFormEditFormType } from '@fastgpt/global/core/app/formEdit/type'; import type { AppFormEditFormType } from '@fastgpt/global/core/app/formEdit/type';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
...@@ -25,7 +24,7 @@ import { getWebLLMModel } from '@/web/common/system/utils'; ...@@ -25,7 +24,7 @@ import { getWebLLMModel } from '@/web/common/system/utils';
import ToolSelect from '../FormComponent/ToolSelector/ToolSelect'; import ToolSelect from '../FormComponent/ToolSelector/ToolSelect';
import { cardStyles } from '../../constants'; import { cardStyles } from '../../constants';
import { SmallAddIcon } from '@chakra-ui/icons'; import { SmallAddIcon } from '@chakra-ui/icons';
import MyIconButton, { MyDeleteIconButton } from '@fastgpt/web/components/common/Icon/button'; import MyIconButton from '@fastgpt/web/components/common/Icon/button';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { useSkillManager } from './hooks/useSkillManager'; import { useSkillManager } from './hooks/useSkillManager';
import { AGENT_SANDBOX_TOOLSET_ID, SANDBOX_ICON } from '@fastgpt/global/core/ai/sandbox/tools'; import { AGENT_SANDBOX_TOOLSET_ID, SANDBOX_ICON } from '@fastgpt/global/core/ai/sandbox/tools';
...@@ -36,6 +35,7 @@ import { useUserStore } from '@/web/support/user/useUserStore'; ...@@ -36,6 +35,7 @@ import { useUserStore } from '@/web/support/user/useUserStore';
import MyTag from '@fastgpt/web/components/common/Tag/index'; import MyTag from '@fastgpt/web/components/common/Tag/index';
import { useAgentSkillSelect } from './hooks/useAgentSkillSelect'; import { useAgentSkillSelect } from './hooks/useAgentSkillSelect';
import { RechargeModal } from '@/components/support/wallet/NotSufficientModal'; import { RechargeModal } from '@/components/support/wallet/NotSufficientModal';
import DatasetCard from '@/components/core/app/DatasetCard';
const DatasetSelectModal = dynamic(() => import('@/components/core/app/DatasetSelectModal')); const DatasetSelectModal = dynamic(() => import('@/components/core/app/DatasetSelectModal'));
const DatasetParamsModal = dynamic(() => import('@/components/core/app/DatasetParamsModal')); const DatasetParamsModal = dynamic(() => import('@/components/core/app/DatasetParamsModal'));
...@@ -58,7 +58,6 @@ const EditForm = ({ ...@@ -58,7 +58,6 @@ const EditForm = ({
appForm: AppFormEditFormType; appForm: AppFormEditFormType;
setAppForm: React.Dispatch<React.SetStateAction<AppFormEditFormType>>; setAppForm: React.Dispatch<React.SetStateAction<AppFormEditFormType>>;
}) => { }) => {
const router = useRouter();
const { t } = useTranslation(); const { t } = useTranslation();
const { feConfigs } = useSystemStore(); const { feConfigs } = useSystemStore();
const { teamPlanStatus } = useUserStore(); const { teamPlanStatus } = useUserStore();
...@@ -466,63 +465,21 @@ const EditForm = ({ ...@@ -466,63 +465,21 @@ const EditForm = ({
</Box> </Box>
)} )}
<Grid gridTemplateColumns={'repeat(2, minmax(0, 1fr))'} gridGap={[2, 4]}> <Grid gridTemplateColumns={'repeat(2, minmax(0, 1fr))'} gridGap={[2, 4]}>
{selectDatasets.map((item) => ( {selectDatasets.map((dataset) => (
<Flex <DatasetCard
key={item.datasetId} key={dataset.datasetId}
overflow={'hidden'} dataset={dataset}
alignItems={'center'} onDelete={(datasetId) => {
p={2} setAppForm((state) => ({
bg={'white'} ...state,
boxShadow={'0 4px 8px -2px rgba(16,24,40,.1),0 2px 4px -2px rgba(16,24,40,.06)'} dataset: {
borderRadius={'md'} ...state.dataset,
border={'base'} datasets:
_hover={{ state.dataset.datasets?.filter((pre) => pre.datasetId !== datasetId) || []
'& .controler': {
display: 'flex'
}
}}
>
<Avatar src={item.avatar} w={'1.5rem'} borderRadius={'sm'} />
<Box
ml={2}
flex={'1 0 0'}
w={0}
className={'textEllipsis'}
fontSize={'sm'}
color={'myGray.900'}
>
{item.name}
</Box>
{/* Icon */}
<Box className="controler" display={['flex', 'none']} alignItems={'center'}>
<MyIconButton
icon={'common/viewLight'}
onClick={() =>
router.push({
pathname: '/dataset/detail',
query: {
datasetId: item.datasetId
}
})
} }
/> }));
<MyDeleteIconButton }}
onClick={() => { />
setAppForm((state) => ({
...state,
dataset: {
...state.dataset,
datasets:
state.dataset.datasets?.filter(
(pre) => pre.datasetId !== item.datasetId
) || []
}
}));
}}
/>
</Box>
</Flex>
))} ))}
</Grid> </Grid>
</Box> </Box>
...@@ -650,7 +607,8 @@ const EditForm = ({ ...@@ -650,7 +607,8 @@ const EditForm = ({
datasetId: item.datasetId, datasetId: item.datasetId,
name: item.name, name: item.name,
avatar: item.avatar, avatar: item.avatar,
vectorModel: item.vectorModel vectorModel: item.vectorModel,
isDeleted: item.isDeleted
}))} }))}
onClose={onCloseKbSelect} onClose={onCloseKbSelect}
onChange={(e) => { onChange={(e) => {
......
...@@ -10,11 +10,9 @@ import { ...@@ -10,11 +10,9 @@ import {
Switch Switch
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import type { AppFormEditFormType } from '@fastgpt/global/core/app/formEdit/type'; import type { AppFormEditFormType } from '@fastgpt/global/core/app/formEdit/type';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
import VariableEdit from '@/components/core/app/VariableEdit'; import VariableEdit from '@/components/core/app/VariableEdit';
import PromptEditor from '@fastgpt/web/components/common/Textarea/PromptEditor'; import PromptEditor from '@fastgpt/web/components/common/Textarea/PromptEditor';
...@@ -32,12 +30,12 @@ import { getWebLLMModel } from '@/web/common/system/utils'; ...@@ -32,12 +30,12 @@ import { getWebLLMModel } from '@/web/common/system/utils';
import ToolSelect from '../FormComponent/ToolSelector/ToolSelect'; import ToolSelect from '../FormComponent/ToolSelector/ToolSelect';
import OptimizerPopover from '@/components/common/PromptEditor/OptimizerPopover'; import OptimizerPopover from '@/components/common/PromptEditor/OptimizerPopover';
import { useSystemStore } from '@/web/common/system/useSystemStore'; import { useSystemStore } from '@/web/common/system/useSystemStore';
import MyIconButton, { MyDeleteIconButton } from '@fastgpt/web/components/common/Icon/button';
import { SmallAddIcon } from '@chakra-ui/icons'; import { SmallAddIcon } from '@chakra-ui/icons';
import { SANDBOX_ICON } from '@fastgpt/global/core/ai/sandbox/tools'; import { SANDBOX_ICON } from '@fastgpt/global/core/ai/sandbox/tools';
import SandboxTipTag from '../../components/SandboxTipTag'; import SandboxTipTag from '../../components/SandboxTipTag';
import SandboxNotSupportTip from '../../components/SandboxNotSupportTip'; import SandboxNotSupportTip from '../../components/SandboxNotSupportTip';
import { useUserStore } from '@/web/support/user/useUserStore'; import { useUserStore } from '@/web/support/user/useUserStore';
import DatasetCard from '@/components/core/app/DatasetCard';
const DatasetSelectModal = dynamic(() => import('@/components/core/app/DatasetSelectModal')); const DatasetSelectModal = dynamic(() => import('@/components/core/app/DatasetSelectModal'));
const DatasetParamsModal = dynamic(() => import('@/components/core/app/DatasetParamsModal')); const DatasetParamsModal = dynamic(() => import('@/components/core/app/DatasetParamsModal'));
...@@ -69,7 +67,6 @@ const EditForm = ({ ...@@ -69,7 +67,6 @@ const EditForm = ({
appForm: AppFormEditFormType; appForm: AppFormEditFormType;
setAppForm: React.Dispatch<React.SetStateAction<AppFormEditFormType>>; setAppForm: React.Dispatch<React.SetStateAction<AppFormEditFormType>>;
}) => { }) => {
const router = useRouter();
const { t } = useTranslation(); const { t } = useTranslation();
const { defaultModels, feConfigs } = useSystemStore(); const { defaultModels, feConfigs } = useSystemStore();
const showSandbox = feConfigs.show_agent_sandbox; const showSandbox = feConfigs.show_agent_sandbox;
...@@ -360,63 +357,21 @@ const EditForm = ({ ...@@ -360,63 +357,21 @@ const EditForm = ({
</Box> </Box>
)} )}
<Grid gridTemplateColumns={'repeat(2, minmax(0, 1fr))'} gridGap={[2, 4]}> <Grid gridTemplateColumns={'repeat(2, minmax(0, 1fr))'} gridGap={[2, 4]}>
{selectDatasets.map((item) => ( {selectDatasets.map((dataset) => (
<Flex <DatasetCard
key={item.datasetId} key={dataset.datasetId}
overflow={'hidden'} dataset={dataset}
alignItems={'center'} onDelete={(datasetId) => {
p={2} setAppForm((state) => ({
bg={'white'} ...state,
boxShadow={'0 4px 8px -2px rgba(16,24,40,.1),0 2px 4px -2px rgba(16,24,40,.06)'} dataset: {
borderRadius={'md'} ...state.dataset,
border={'base'} datasets:
_hover={{ state.dataset.datasets?.filter((pre) => pre.datasetId !== datasetId) || []
'& .controler': {
display: 'flex'
}
}}
>
<Avatar src={item.avatar} w={'1.5rem'} borderRadius={'sm'} />
<Box
ml={2}
flex={'1 0 0'}
w={0}
className={'textEllipsis'}
fontSize={'sm'}
color={'myGray.900'}
>
{item.name}
</Box>
{/* Icon */}
<Box className="controler" display={['flex', 'none']} alignItems={'center'}>
<MyIconButton
icon={'common/viewLight'}
onClick={() =>
router.push({
pathname: '/dataset/detail',
query: {
datasetId: item.datasetId
}
})
} }
/> }));
<MyDeleteIconButton }}
onClick={() => { />
setAppForm((state) => ({
...state,
dataset: {
...state.dataset,
datasets:
state.dataset.datasets?.filter(
(pre) => pre.datasetId !== item.datasetId
) || []
}
}));
}}
/>
</Box>
</Flex>
))} ))}
</Grid> </Grid>
</Box> </Box>
...@@ -543,7 +498,8 @@ const EditForm = ({ ...@@ -543,7 +498,8 @@ const EditForm = ({
datasetId: item.datasetId, datasetId: item.datasetId,
name: item.name, name: item.name,
avatar: item.avatar, avatar: item.avatar,
vectorModel: item.vectorModel vectorModel: item.vectorModel,
isDeleted: item.isDeleted
}))} }))}
onClose={onCloseDatasetSelect} onClose={onCloseDatasetSelect}
onChange={(e) => { onChange={(e) => {
......
...@@ -14,7 +14,7 @@ import dynamic from 'next/dynamic'; ...@@ -14,7 +14,7 @@ import dynamic from 'next/dynamic';
import { Box, Button, Flex } from '@chakra-ui/react'; import { Box, Button, Flex } from '@chakra-ui/react';
import { type FieldErrors, useForm } from 'react-hook-form'; import { type FieldErrors, useForm } from 'react-hook-form';
import { VariableInputEnum } from '@fastgpt/global/core/workflow/constants'; import { VariableInputEnum } from '@fastgpt/global/core/workflow/constants';
import { checkInputIsReference } from '@fastgpt/global/core/workflow/utils'; import { nodeInputIsReference } from '@fastgpt/global/core/workflow/utils';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { AppContext } from '../../../context'; import { AppContext } from '../../../context';
...@@ -153,7 +153,7 @@ export const useDebug = () => { ...@@ -153,7 +153,7 @@ export const useDebug = () => {
// BUG: 工具调用的情况下,无法填写非必填 // BUG: 工具调用的情况下,无法填写非必填
const renderInputs = runtimeNode.inputs.filter((input) => { const renderInputs = runtimeNode.inputs.filter((input) => {
if (runtimeNode.flowNodeType === FlowNodeTypeEnum.pluginInput) return true; if (runtimeNode.flowNodeType === FlowNodeTypeEnum.pluginInput) return true;
if (checkInputIsReference(input)) return true; if (nodeInputIsReference(input)) return true;
if (!input.value) return true; if (!input.value) return true;
}); });
......
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants'; import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import { checkInputIsReference } from '@fastgpt/global/core/workflow/utils'; import { nodeInputIsReference } from '@fastgpt/global/core/workflow/utils';
import type { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io'; import type { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
const primitiveValueTypes = new Set<WorkflowIOValueTypeEnum>([ const primitiveValueTypes = new Set<WorkflowIOValueTypeEnum>([
...@@ -9,7 +9,7 @@ const primitiveValueTypes = new Set<WorkflowIOValueTypeEnum>([ ...@@ -9,7 +9,7 @@ const primitiveValueTypes = new Set<WorkflowIOValueTypeEnum>([
]); ]);
export const getDebugInputFormValue = (input: FlowNodeInputItemType) => { export const getDebugInputFormValue = (input: FlowNodeInputItemType) => {
if (checkInputIsReference(input)) return undefined; if (nodeInputIsReference(input)) return undefined;
const value = input.value ?? input.defaultValue; const value = input.value ?? input.defaultValue;
if (typeof value === 'object' && value !== null) { if (typeof value === 'object' && value !== null) {
......
...@@ -46,6 +46,7 @@ import SandboxTipTag from '@/pageComponents/app/detail/components/SandboxTipTag' ...@@ -46,6 +46,7 @@ import SandboxTipTag from '@/pageComponents/app/detail/components/SandboxTipTag'
import { RechargeModal } from '@/components/support/wallet/NotSufficientModal'; import { RechargeModal } from '@/components/support/wallet/NotSufficientModal';
import { useToast } from '@fastgpt/web/hooks/useToast'; import { useToast } from '@fastgpt/web/hooks/useToast';
import MyTag from '@fastgpt/web/components/common/Tag/index'; import MyTag from '@fastgpt/web/components/common/Tag/index';
import DatasetCard from '@/components/core/app/DatasetCard';
const PromptEditor = dynamic(() => import('@fastgpt/web/components/common/Textarea/PromptEditor')); const PromptEditor = dynamic(() => import('@fastgpt/web/components/common/Textarea/PromptEditor'));
const SkillSelectModal = dynamic( const SkillSelectModal = dynamic(
...@@ -867,28 +868,7 @@ const NodeAgent = ({ data, selected }: NodeProps<FlowNodeItemType>) => { ...@@ -867,28 +868,7 @@ const NodeAgent = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
{t('common:Choose')} {t('common:Choose')}
</Button> </Button>
{selectedDatasets.map((dataset) => ( {selectedDatasets.map((dataset) => (
<Flex <DatasetCard key={dataset.datasetId} dataset={dataset} />
key={dataset.datasetId}
alignItems={'center'}
h={10}
boxShadow={'sm'}
bg={'white'}
border={'base'}
px={2}
borderRadius={'md'}
>
<Avatar src={dataset.avatar} w={'18px'} borderRadius={'xs'} />
<Box
ml={1.5}
flex={'1 0 0'}
w={0}
className="textEllipsis"
fontWeight={'bold'}
fontSize={['sm', 'sm']}
>
{dataset.name}
</Box>
</Flex>
))} ))}
</Grid> </Grid>
{isOpenDatasetSelect && ( {isOpenDatasetSelect && (
...@@ -897,7 +877,8 @@ const NodeAgent = ({ data, selected }: NodeProps<FlowNodeItemType>) => { ...@@ -897,7 +877,8 @@ const NodeAgent = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
datasetId: d.datasetId, datasetId: d.datasetId,
name: d.name, name: d.name,
avatar: d.avatar, avatar: d.avatar,
vectorModel: d.vectorModel vectorModel: d.vectorModel,
isDeleted: d.isDeleted
}))} }))}
onChange={(e) => { onChange={(e) => {
if (!datasetSelectInput) return; if (!datasetSelectInput) return;
......
import React, { useEffect, useMemo, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import type { RenderInputProps } from '../type'; import type { RenderInputProps } from '../type';
import { Box, Button, Flex, Grid, Switch, useDisclosure } from '@chakra-ui/react'; import { Box, Button, Flex, Grid, Switch, useDisclosure } from '@chakra-ui/react';
import { type SelectedDatasetType } from '@fastgpt/global/core/workflow/type/io'; import { type SelectedDatasetType } from '@fastgpt/global/core/workflow/type/io';
import Avatar from '@fastgpt/web/components/common/Avatar';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { DatasetSearchModeEnum } from '@fastgpt/global/core/dataset/constants'; import { DatasetSearchModeEnum } from '@fastgpt/global/core/dataset/constants';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
...@@ -11,6 +10,7 @@ import { useContextSelector } from 'use-context-selector'; ...@@ -11,6 +10,7 @@ import { useContextSelector } from 'use-context-selector';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip'; import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { WorkflowActionsContext } from '@/pageComponents/app/detail/WorkflowComponents/context/workflowActionsContext'; import { WorkflowActionsContext } from '@/pageComponents/app/detail/WorkflowComponents/context/workflowActionsContext';
import DatasetCard from '@/components/core/app/DatasetCard';
const DatasetSelectModal = dynamic(() => import('@/components/core/app/DatasetSelectModal')); const DatasetSelectModal = dynamic(() => import('@/components/core/app/DatasetSelectModal'));
...@@ -52,6 +52,21 @@ export const SelectDatasetRender = React.memo(function SelectDatasetRender({ ...@@ -52,6 +52,21 @@ export const SelectDatasetRender = React.memo(function SelectDatasetRender({
}); });
}, [inputs]); }, [inputs]);
const onDeleteDataset = useCallback(
(datasetId: string) => {
onChangeNode({
nodeId,
key: item.key,
type: 'updateInput',
value: {
...item,
value: selectedDatasets.filter((dataset) => dataset.datasetId !== datasetId)
}
});
},
[item, nodeId, onChangeNode, selectedDatasets]
);
const Render = useMemo(() => { const Render = useMemo(() => {
return ( return (
<> <>
...@@ -68,29 +83,8 @@ export const SelectDatasetRender = React.memo(function SelectDatasetRender({ ...@@ -68,29 +83,8 @@ export const SelectDatasetRender = React.memo(function SelectDatasetRender({
> >
{t('common:Choose')} {t('common:Choose')}
</Button> </Button>
{selectedDatasets.map((item) => ( {selectedDatasets.map((dataset) => (
<Flex <DatasetCard key={dataset.datasetId} dataset={dataset} onDelete={onDeleteDataset} />
key={item.datasetId}
alignItems={'center'}
h={10}
boxShadow={'sm'}
bg={'white'}
border={'base'}
px={2}
borderRadius={'md'}
>
<Avatar src={item.avatar} w={'18px'} borderRadius={'xs'} />
<Box
ml={1.5}
flex={'1 0 0'}
w={0}
className="textEllipsis"
fontWeight={'bold'}
fontSize={['sm', 'sm']}
>
{item.name}
</Box>
</Flex>
))} ))}
</Grid> </Grid>
{isOpenDatasetSelect && ( {isOpenDatasetSelect && (
...@@ -99,7 +93,8 @@ export const SelectDatasetRender = React.memo(function SelectDatasetRender({ ...@@ -99,7 +93,8 @@ export const SelectDatasetRender = React.memo(function SelectDatasetRender({
datasetId: item.datasetId, datasetId: item.datasetId,
name: item.name, name: item.name,
avatar: item.avatar, avatar: item.avatar,
vectorModel: item.vectorModel vectorModel: item.vectorModel,
isDeleted: item.isDeleted
}))} }))}
onChange={(e) => { onChange={(e) => {
onChangeNode({ onChangeNode({
...@@ -124,6 +119,7 @@ export const SelectDatasetRender = React.memo(function SelectDatasetRender({ ...@@ -124,6 +119,7 @@ export const SelectDatasetRender = React.memo(function SelectDatasetRender({
onChangeNode, onChangeNode,
onCloseDatasetSelect, onCloseDatasetSelect,
onOpenDatasetSelect, onOpenDatasetSelect,
onDeleteDataset,
selectedDatasets, selectedDatasets,
t t
]); ]);
......
...@@ -10,7 +10,6 @@ import { ...@@ -10,7 +10,6 @@ import {
type FlowNodeItemType, type FlowNodeItemType,
type StoreNodeItemType type StoreNodeItemType
} from '@fastgpt/global/core/workflow/type/node'; } from '@fastgpt/global/core/workflow/type/node';
import type { SelectedAgentSkillItemType } from '@fastgpt/global/core/app/formEdit/type';
import { type TFunction } from 'i18next'; import { type TFunction } from 'i18next';
import { type Edge, type Node } from 'reactflow'; import { type Edge, type Node } from 'reactflow';
...@@ -21,26 +20,6 @@ export const uiWorkflow2StoreWorkflow = ({ ...@@ -21,26 +20,6 @@ export const uiWorkflow2StoreWorkflow = ({
nodes: Node<FlowNodeItemType, string | undefined>[]; nodes: Node<FlowNodeItemType, string | undefined>[];
edges: Edge<any>[]; edges: Edge<any>[];
}) => { }) => {
const formatInputs = (inputs: StoreNodeItemType['inputs']) =>
inputs.map((input) => {
if (input.key !== NodeInputKeyEnum.skills || !Array.isArray(input.value)) {
return input;
}
// isDeleted 只用于编辑页提示,保存时不能写回应用配置。
return {
...input,
value: (input.value as SelectedAgentSkillItemType[]).map(
({ skillId, name, description, avatar }) => ({
skillId,
name,
description,
...(avatar === undefined ? {} : { avatar })
})
)
};
});
const formatNodes: StoreNodeItemType[] = nodes.map((item) => ({ const formatNodes: StoreNodeItemType[] = nodes.map((item) => ({
nodeId: item.data.nodeId, nodeId: item.data.nodeId,
parentNodeId: item.data.parentNodeId, parentNodeId: item.data.parentNodeId,
...@@ -52,7 +31,7 @@ export const uiWorkflow2StoreWorkflow = ({ ...@@ -52,7 +31,7 @@ export const uiWorkflow2StoreWorkflow = ({
showStatus: item.data.showStatus, showStatus: item.data.showStatus,
position: item.position, position: item.position,
version: item.data.version, version: item.data.version,
inputs: formatInputs(item.data.inputs), inputs: item.data.inputs,
outputs: item.data.outputs, outputs: item.data.outputs,
isFolded: item.data.isFolded, isFolded: item.data.isFolded,
pluginId: item.data.pluginId, pluginId: item.data.pluginId,
......
...@@ -34,7 +34,10 @@ import { removeUnauthModels } from '@fastgpt/global/core/workflow/utils'; ...@@ -34,7 +34,10 @@ import { removeUnauthModels } from '@fastgpt/global/core/workflow/utils';
import { getS3AvatarSource } from '@fastgpt/service/common/s3/sources/avatar'; import { getS3AvatarSource } from '@fastgpt/service/common/s3/sources/avatar';
import { isS3ObjectKey } from '@fastgpt/service/common/s3/utils'; import { isS3ObjectKey } from '@fastgpt/service/common/s3/utils';
import { MongoAppTemplate } from '@fastgpt/service/core/app/templates/templateSchema'; import { MongoAppTemplate } from '@fastgpt/service/core/app/templates/templateSchema';
import { updateParentFoldersUpdateTime } from '@fastgpt/service/core/app/controller'; import {
beforeUpdateAppFormat,
updateParentFoldersUpdateTime
} from '@fastgpt/service/core/app/controller';
import { copyAvatarImage } from '@fastgpt/service/common/file/image/controller'; import { copyAvatarImage } from '@fastgpt/service/common/file/image/controller';
import { extractAppResourceRefsFromNodes } from '@fastgpt/service/core/app/resourceRefs'; import { extractAppResourceRefsFromNodes } from '@fastgpt/service/core/app/resourceRefs';
...@@ -156,6 +159,10 @@ export const onCreateApp = async ({ ...@@ -156,6 +159,10 @@ export const onCreateApp = async ({
} }
} }
beforeUpdateAppFormat({
nodes: modules
});
const create = async (session: ClientSession) => { const create = async (session: ClientSession) => {
const resourceRefs = extractAppResourceRefsFromNodes(modules); const resourceRefs = extractAppResourceRefsFromNodes(modules);
const _avatar = await (async () => { const _avatar = await (async () => {
......
...@@ -82,9 +82,9 @@ describe('POST /api/core/ai/skill/list', () => { ...@@ -82,9 +82,9 @@ describe('POST /api/core/ai/skill/list', () => {
} }
]); ]);
const createSkillNode = (skillId: string): StoreNodeItemType => const createSkillNode = (skill: { _id: unknown; name: string }): StoreNodeItemType =>
({ ({
nodeId: `node-${skillId}`, nodeId: `node-${String(skill._id)}`,
name: 'Agent', name: 'Agent',
flowNodeType: FlowNodeTypeEnum.agent, flowNodeType: FlowNodeTypeEnum.agent,
inputs: [ inputs: [
...@@ -93,7 +93,12 @@ describe('POST /api/core/ai/skill/list', () => { ...@@ -93,7 +93,12 @@ describe('POST /api/core/ai/skill/list', () => {
label: 'Skills', label: 'Skills',
renderTypeList: [FlowNodeInputTypeEnum.selectSkill], renderTypeList: [FlowNodeInputTypeEnum.selectSkill],
valueType: WorkflowIOValueTypeEnum.arrayObject, valueType: WorkflowIOValueTypeEnum.arrayObject,
value: [{ skillId }] value: [
{
skillId: String(skill._id),
name: skill.name
}
]
} }
], ],
outputs: [] outputs: []
...@@ -102,7 +107,7 @@ describe('POST /api/core/ai/skill/list', () => { ...@@ -102,7 +107,7 @@ describe('POST /api/core/ai/skill/list', () => {
const appId = await onCreateApp({ const appId = await onCreateApp({
name: 'Skill Ref App', name: 'Skill Ref App',
type: AppTypeEnum.chatAgent, type: AppTypeEnum.chatAgent,
modules: [createSkillNode(String(publishedSkill._id))], modules: [createSkillNode(publishedSkill)],
edges: [], edges: [],
chatConfig: {}, chatConfig: {},
teamId: user.teamId, teamId: user.teamId,
...@@ -116,7 +121,7 @@ describe('POST /api/core/ai/skill/list', () => { ...@@ -116,7 +121,7 @@ describe('POST /api/core/ai/skill/list', () => {
auth: user, auth: user,
query: { appId }, query: { appId },
body: { body: {
nodes: [createSkillNode(String(draftSkill._id))], nodes: [createSkillNode(draftSkill)],
edges: [], edges: [],
chatConfig: {}, chatConfig: {},
isPublish: false, isPublish: false,
...@@ -149,7 +154,7 @@ describe('POST /api/core/ai/skill/list', () => { ...@@ -149,7 +154,7 @@ describe('POST /api/core/ai/skill/list', () => {
auth: user, auth: user,
query: { appId }, query: { appId },
body: { body: {
nodes: [createSkillNode(String(draftSkill._id))], nodes: [createSkillNode(draftSkill)],
edges: [], edges: [],
chatConfig: {}, chatConfig: {},
isPublish: true, isPublish: true,
......
...@@ -4,7 +4,10 @@ import { ...@@ -4,7 +4,10 @@ import {
filterExportModules, filterExportModules,
getEditorVariables getEditorVariables
} from '@/pageComponents/app/detail/WorkflowComponents/utils'; } from '@/pageComponents/app/detail/WorkflowComponents/utils';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import {
FlowNodeInputTypeEnum,
FlowNodeTypeEnum
} from '@fastgpt/global/core/workflow/node/constant';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import type { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node'; import type { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node';
import type { AppDetailType } from '@fastgpt/global/core/app/type'; import type { AppDetailType } from '@fastgpt/global/core/app/type';
...@@ -216,7 +219,7 @@ describe('WorkflowComponents utils', () => { ...@@ -216,7 +219,7 @@ describe('WorkflowComponents utils', () => {
]); ]);
}); });
it('should strip skill deleted marker when saving workflow', () => { it('should keep selected skill snapshot for later server-side save formatting', () => {
const nodes = [ const nodes = [
{ {
data: { data: {
...@@ -256,7 +259,8 @@ describe('WorkflowComponents utils', () => { ...@@ -256,7 +259,8 @@ describe('WorkflowComponents utils', () => {
{ {
skillId: 'skill-1', skillId: 'skill-1',
name: 'Deleted Skill', name: 'Deleted Skill',
description: '' description: '',
isDeleted: true
}, },
{ {
skillId: 'skill-2', skillId: 'skill-2',
...@@ -265,6 +269,91 @@ describe('WorkflowComponents utils', () => { ...@@ -265,6 +269,91 @@ describe('WorkflowComponents utils', () => {
} }
]); ]);
}); });
it('should keep dataset reference value when saving workflow', () => {
const referenceValue = ['sourceNode', 'datasets'];
const nodes = [
{
data: {
nodeId: 'datasetNode',
name: 'Dataset Search',
intro: '',
avatar: '',
flowNodeType: FlowNodeTypeEnum.datasetSearchNode,
showStatus: true,
inputs: [
{
key: NodeInputKeyEnum.datasetSelectList,
renderTypeList: [
FlowNodeInputTypeEnum.selectDataset,
FlowNodeInputTypeEnum.reference
],
selectedTypeIndex: 1,
value: referenceValue
}
],
outputs: []
},
position: { x: 0, y: 0 }
}
];
const result = uiWorkflow2StoreWorkflow({ nodes, edges: [] });
expect(result.nodes[0].inputs[0].value).toEqual(referenceValue);
});
it('should keep selected dataset snapshot for later server-side save formatting', () => {
const nodes = [
{
data: {
nodeId: 'datasetNode',
name: 'Dataset Search',
intro: '',
avatar: '',
flowNodeType: FlowNodeTypeEnum.datasetSearchNode,
showStatus: true,
inputs: [
{
key: NodeInputKeyEnum.datasetSelectList,
renderTypeList: [
FlowNodeInputTypeEnum.selectDataset,
FlowNodeInputTypeEnum.reference
],
selectedTypeIndex: 0,
value: [
{
datasetId: 'dataset-1',
avatar: 'avatar.png',
name: 'Deleted Dataset',
vectorModel: {
model: 'text-embedding'
},
isDeleted: true
}
]
}
],
outputs: []
},
position: { x: 0, y: 0 }
}
];
const result = uiWorkflow2StoreWorkflow({ nodes, edges: [] });
expect(result.nodes[0].inputs[0].value).toEqual([
{
datasetId: 'dataset-1',
avatar: 'avatar.png',
name: 'Deleted Dataset',
vectorModel: {
model: 'text-embedding'
},
isDeleted: true
}
]);
});
}); });
describe('filterExportModules', () => { describe('filterExportModules', () => {
......
import { describe, expect, it } from 'vitest';
import { beforeUpdateAppFormat } from '@fastgpt/service/core/app/controller';
import {
FlowNodeInputTypeEnum,
FlowNodeTypeEnum
} from '@fastgpt/global/core/workflow/node/constant';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import type { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node';
describe('beforeUpdateAppFormat', () => {
it('保存前统一压缩知识库选择项,去掉编辑态删除标记和快照字段', () => {
const nodes = [
{
flowNodeType: FlowNodeTypeEnum.datasetSearchNode,
inputs: [
{
key: NodeInputKeyEnum.datasetSelectList,
renderTypeList: [FlowNodeInputTypeEnum.selectDataset, FlowNodeInputTypeEnum.reference],
selectedTypeIndex: 0,
value: [
{
datasetId: 'dataset-1',
avatar: 'avatar.png',
name: 'Deleted Dataset',
vectorModel: {
model: 'text-embedding'
},
isDeleted: true
}
]
}
]
} as StoreNodeItemType
];
beforeUpdateAppFormat({ nodes });
expect(nodes[0].inputs[0].value).toEqual([
{
datasetId: 'dataset-1'
}
]);
});
it('保存前兼容旧版单对象知识库选择项', () => {
const nodes = [
{
flowNodeType: FlowNodeTypeEnum.datasetSearchNode,
inputs: [
{
key: NodeInputKeyEnum.datasetSelectList,
renderTypeList: [FlowNodeInputTypeEnum.selectDataset, FlowNodeInputTypeEnum.reference],
selectedTypeIndex: 0,
value: {
datasetId: 'dataset-legacy',
avatar: 'avatar.png',
name: 'Legacy Dataset',
vectorModel: {
model: 'text-embedding'
}
}
}
]
} as StoreNodeItemType
];
beforeUpdateAppFormat({ nodes });
expect(nodes[0].inputs[0].value).toEqual([
{
datasetId: 'dataset-legacy'
}
]);
});
it('保存前兼容已压缩的知识库选择项数组', () => {
const nodes = [
{
flowNodeType: FlowNodeTypeEnum.datasetSearchNode,
inputs: [
{
key: NodeInputKeyEnum.datasetSelectList,
renderTypeList: [FlowNodeInputTypeEnum.selectDataset, FlowNodeInputTypeEnum.reference],
selectedTypeIndex: 0,
value: [
{
datasetId: 'dataset-1'
},
{
datasetId: 'dataset-2'
}
]
}
]
} as StoreNodeItemType
];
beforeUpdateAppFormat({ nodes });
expect(nodes[0].inputs[0].value).toEqual([
{
datasetId: 'dataset-1'
},
{
datasetId: 'dataset-2'
}
]);
});
it('保存前统一压缩 Agent datasetParams 中的知识库选择项', () => {
const nodes = [
{
flowNodeType: FlowNodeTypeEnum.agent,
inputs: [
{
key: NodeInputKeyEnum.datasetParams,
value: {
datasets: [
{
datasetId: 'dataset-1',
avatar: 'avatar.png',
name: 'Deleted Dataset',
vectorModel: {
model: 'text-embedding'
},
isDeleted: true
}
],
similarity: 0.5,
limit: 5
}
}
]
} as StoreNodeItemType
];
beforeUpdateAppFormat({ nodes });
expect(nodes[0].inputs[0].value).toMatchObject({
datasets: [
{
datasetId: 'dataset-1'
}
],
similarity: 0.5,
limit: 5
});
});
it('保存前保留知识库选择输入的引用模式值', () => {
const referenceValue = ['sourceNode', 'datasets'];
const nodes = [
{
flowNodeType: FlowNodeTypeEnum.datasetSearchNode,
inputs: [
{
key: NodeInputKeyEnum.datasetSelectList,
renderTypeList: [FlowNodeInputTypeEnum.selectDataset, FlowNodeInputTypeEnum.reference],
selectedTypeIndex: 1,
value: referenceValue
}
]
} as StoreNodeItemType
];
beforeUpdateAppFormat({ nodes });
expect(nodes[0].inputs[0].value).toBe(referenceValue);
});
it('保存前遇到非法知识库选择项时抛错,避免清空后继续保存', () => {
const nodes = [
{
flowNodeType: FlowNodeTypeEnum.datasetSearchNode,
inputs: [
{
key: NodeInputKeyEnum.datasetSelectList,
renderTypeList: [FlowNodeInputTypeEnum.selectDataset, FlowNodeInputTypeEnum.reference],
selectedTypeIndex: 0,
value: [
{
name: 'Invalid Dataset'
}
]
}
]
} as StoreNodeItemType
];
expect(() => beforeUpdateAppFormat({ nodes })).toThrow();
});
it('保存前移除 Agent Skill 的编辑态删除标记和展示快照字段', () => {
const nodes = [
{
flowNodeType: FlowNodeTypeEnum.agent,
inputs: [
{
key: NodeInputKeyEnum.skills,
renderTypeList: [FlowNodeInputTypeEnum.selectSkill, FlowNodeInputTypeEnum.reference],
selectedTypeIndex: 0,
value: [
{
skillId: 'skill-1',
name: 'Deleted Skill',
description: 'Snapshot description',
avatar: 'skill-avatar.png',
isDeleted: true
},
{
skillId: 'skill-2',
name: 'Normal Skill',
description: '',
isDeleted: false
}
]
}
]
} as StoreNodeItemType
];
beforeUpdateAppFormat({ nodes });
expect(nodes[0].inputs[0].value).toEqual([
{
skillId: 'skill-1',
name: 'Deleted Skill'
},
{
skillId: 'skill-2',
name: 'Normal Skill'
}
]);
});
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment