feat: file selector render

This commit is contained in:
archer 2025-12-10 13:22:52 +08:00
parent 4efd805961
commit cdae418a9c
No known key found for this signature in database
GPG Key ID: 4446499B846D4A9E
17 changed files with 183 additions and 36 deletions

View File

@ -32,6 +32,7 @@ curl --location --request POST 'https://{{host}}/api/admin/initv4144' \
6. 支持配置对话文件白名单。
7. S3 支持 pathStyle 配置。
8. 支持通过 Sealos 来进行多租户自定义域名配置。
9. 工作流中引用工具时,文件输入支持手动填写(原本只支持变量引用)。
## ⚙️ 优化

View File

@ -119,7 +119,7 @@
"document/content/docs/upgrading/4-14/4141.mdx": "2025-11-19T10:15:27+08:00",
"document/content/docs/upgrading/4-14/4142.mdx": "2025-11-18T19:27:14+08:00",
"document/content/docs/upgrading/4-14/4143.mdx": "2025-11-26T20:52:05+08:00",
"document/content/docs/upgrading/4-14/4144.mdx": "2025-12-09T23:33:32+08:00",
"document/content/docs/upgrading/4-14/4144.mdx": "2025-12-10T11:23:18+08:00",
"document/content/docs/upgrading/4-8/40.mdx": "2025-08-02T19:38:37+08:00",
"document/content/docs/upgrading/4-8/41.mdx": "2025-08-02T19:38:37+08:00",
"document/content/docs/upgrading/4-8/42.mdx": "2025-08-02T19:38:37+08:00",

View File

@ -209,14 +209,15 @@ export type DispatchNodeResponseType = {
headers?: Record<string, any>;
httpResult?: Record<string, any>;
// plugin output
// Tool
toolInput?: Record<string, any>;
pluginOutput?: Record<string, any>;
pluginDetail?: ChatHistoryItemResType[];
// if-else
ifElseResult?: string;
// tool
// tool call
toolCallInputTokens?: number;
toolCallOutputTokens?: number;
toolDetail?: ChatHistoryItemResType[];
@ -225,9 +226,6 @@ export type DispatchNodeResponseType = {
// code
codeLog?: string;
// plugin
pluginOutput?: Record<string, any>;
// read files
readFilesResult?: string;
readFiles?: ReadFileNodeResponse;

View File

@ -54,6 +54,7 @@ export const dispatchRunTool = async (props: RunToolProps): Promise<RunToolRespo
} = props;
const systemToolId = toolConfig?.systemTool?.toolId;
let toolInput: Record<string, any> = {};
try {
// run system tool
@ -78,10 +79,11 @@ export const dispatchRunTool = async (props: RunToolProps): Promise<RunToolRespo
return dbPlugin?.inputListVal || {};
}
})();
toolInput = Object.fromEntries(
Object.entries(params).filter(([key]) => key !== NodeInputKeyEnum.systemInputConfig)
);
const inputs = {
...Object.fromEntries(
Object.entries(params).filter(([key]) => key !== NodeInputKeyEnum.systemInputConfig)
),
...toolInput,
...inputConfigParams
};
@ -132,6 +134,7 @@ export const dispatchRunTool = async (props: RunToolProps): Promise<RunToolRespo
return {
data: res.error,
[DispatchNodeResponseKeyEnum.nodeResponse]: {
toolInput,
toolRes: res.error,
moduleLogo: avatar
},
@ -148,6 +151,7 @@ export const dispatchRunTool = async (props: RunToolProps): Promise<RunToolRespo
return {
error: res.error,
[DispatchNodeResponseKeyEnum.nodeResponse]: {
toolInput,
error: res.error,
moduleLogo: avatar
},
@ -179,6 +183,7 @@ export const dispatchRunTool = async (props: RunToolProps): Promise<RunToolRespo
data: result,
[DispatchNodeResponseKeyEnum.answerText]: answerText,
[DispatchNodeResponseKeyEnum.nodeResponse]: {
toolInput,
toolRes: result,
moduleLogo: avatar,
totalPoints: usagePoints
@ -213,10 +218,12 @@ export const dispatchRunTool = async (props: RunToolProps): Promise<RunToolRespo
});
props.mcpClientMemory[url] = mcpClient;
toolInput = params;
const result = await mcpClient.toolCall({ toolName, params, closeConnection: false });
return {
data: { [NodeOutputKeyEnum.rawResponse]: result },
[DispatchNodeResponseKeyEnum.nodeResponse]: {
toolInput,
toolRes: result,
moduleLogo: avatar
},
@ -241,6 +248,7 @@ export const dispatchRunTool = async (props: RunToolProps): Promise<RunToolRespo
throw new Error(`HTTP tool ${toolName} not found`);
}
toolInput = params;
const { data, errorMsg } = await runHTTPTool({
baseUrl: baseUrl || '',
toolPath: httpTool.path,
@ -262,6 +270,7 @@ export const dispatchRunTool = async (props: RunToolProps): Promise<RunToolRespo
return {
error: { [NodeOutputKeyEnum.errorText]: errorMsg },
[DispatchNodeResponseKeyEnum.nodeResponse]: {
toolInput,
toolRes: errorMsg,
moduleLogo: avatar
},
@ -274,6 +283,7 @@ export const dispatchRunTool = async (props: RunToolProps): Promise<RunToolRespo
return {
data: { [NodeOutputKeyEnum.rawResponse]: data, ...(typeof data === 'object' ? data : {}) },
[DispatchNodeResponseKeyEnum.nodeResponse]: {
toolInput,
toolRes: data,
moduleLogo: avatar
},
@ -290,6 +300,7 @@ export const dispatchRunTool = async (props: RunToolProps): Promise<RunToolRespo
storeSecret: headerSecret
})
});
toolInput = restParams;
const result = await mcpClient.toolCall({ toolName, params: restParams });
return {
@ -297,6 +308,7 @@ export const dispatchRunTool = async (props: RunToolProps): Promise<RunToolRespo
[NodeOutputKeyEnum.rawResponse]: result
},
[DispatchNodeResponseKeyEnum.nodeResponse]: {
toolInput,
toolRes: result,
moduleLogo: avatar
},
@ -318,6 +330,7 @@ export const dispatchRunTool = async (props: RunToolProps): Promise<RunToolRespo
return getNodeErrResponse({
error,
customNodeResponse: {
toolInput,
moduleLogo: avatar
}
});

View File

@ -105,6 +105,12 @@ export const dispatchRunPlugin = async (props: RunPluginProps): Promise<RunPlugi
let val = data[input.key] ?? input.value;
if (input.renderTypeList.includes(FlowNodeInputTypeEnum.password)) {
val = anyValueDecrypt(val);
} else if (
input.renderTypeList.includes(FlowNodeInputTypeEnum.fileSelect) &&
Array.isArray(val) &&
data[input.key]
) {
data[input.key] = val.map((item) => item.url);
}
return {
@ -172,6 +178,7 @@ export const dispatchRunPlugin = async (props: RunPluginProps): Promise<RunPlugi
[DispatchNodeResponseKeyEnum.nodeResponse]: {
moduleLogo: plugin.avatar,
totalPoints: usagePoints,
toolInput: data,
pluginOutput: output?.pluginOutput,
pluginDetail: pluginData?.permission?.hasWritePer // Not system plugin
? flowResponses.filter((item) => {

View File

@ -56,6 +56,8 @@ export const dispatchPluginInput = async (
return {
data: {
...params,
// 旧版本适配
[NodeOutputKeyEnum.userFiles]: files
.map((item) => {
return item?.url ?? '';

View File

@ -51,61 +51,56 @@ const NodeInputSelect = ({
{
type: FlowNodeInputTypeEnum.textarea,
icon: FlowNodeInputMap[FlowNodeInputTypeEnum.textarea].icon,
title: t('common:core.workflow.inputType.Manual input')
},
{
type: FlowNodeInputTypeEnum.JSONEditor,
icon: FlowNodeInputMap[FlowNodeInputTypeEnum.JSONEditor].icon,
title: t('common:core.workflow.inputType.Manual input')
},
{
type: FlowNodeInputTypeEnum.addInputParam,
icon: FlowNodeInputMap[FlowNodeInputTypeEnum.addInputParam].icon,
title: t('common:core.workflow.inputType.dynamicTargetInput')
},
{
type: FlowNodeInputTypeEnum.selectLLMModel,
icon: FlowNodeInputMap[FlowNodeInputTypeEnum.selectLLMModel].icon,
title: t('common:core.workflow.inputType.Manual select')
},
{
type: FlowNodeInputTypeEnum.settingLLMModel,
icon: FlowNodeInputMap[FlowNodeInputTypeEnum.settingLLMModel].icon,
title: t('common:core.workflow.inputType.Manual select')
},
{
type: FlowNodeInputTypeEnum.selectDataset,
icon: FlowNodeInputMap[FlowNodeInputTypeEnum.selectDataset].icon,
title: t('common:core.workflow.inputType.Manual select')
},
{
type: FlowNodeInputTypeEnum.selectDatasetParamsModal,
icon: FlowNodeInputMap[FlowNodeInputTypeEnum.selectDatasetParamsModal].icon,
title: t('common:core.workflow.inputType.Manual select')
},
{
type: FlowNodeInputTypeEnum.settingDatasetQuotePrompt,
icon: FlowNodeInputMap[FlowNodeInputTypeEnum.settingDatasetQuotePrompt].icon,
title: t('common:core.workflow.inputType.Manual input')
},
{
type: FlowNodeInputTypeEnum.hidden,
icon: FlowNodeInputMap[FlowNodeInputTypeEnum.hidden].icon,
title: t('common:core.workflow.inputType.Manual input')
},
{
type: FlowNodeInputTypeEnum.custom,
icon: FlowNodeInputMap[FlowNodeInputTypeEnum.custom].icon,
title: t('common:core.workflow.inputType.Manual input')
},
{
type: FlowNodeInputTypeEnum.fileSelect,
icon: FlowNodeInputMap[FlowNodeInputTypeEnum.fileSelect].icon,
title: t('common:core.workflow.inputType.Manual input')
}
]);
@ -122,7 +117,7 @@ const NodeInputSelect = ({
onChange(input.type);
}
})),
[renderType]
[onChange, renderType]
);
const filterMenuList = useMemo(

View File

@ -14,7 +14,7 @@
"citations": "{{num}} References",
"clear_input_value": "Clear input",
"click_contextual_preview": "Click to see contextual preview",
"click_to_add_url": "Click to add link",
"click_to_add_url": "Enter file link",
"completion_finish_close": "Disconnection",
"completion_finish_content_filter": "Trigger safe wind control",
"completion_finish_function_call": "Function Calls",
@ -51,6 +51,7 @@
"home.no_available_tools": "No tools available",
"home.select_tools": "Select Tool",
"home.tools": "Tool: {{num}}",
"images_collection_not_supported": "Image collection is not supported open the original file",
"in_progress": "In Progress",
"input_guide": "Input Guide",
"input_guide_lexicon": "Lexicon",
@ -75,7 +76,6 @@
"query_extension_result": "Problem optimization results",
"question_tip": "From top to bottom, the response order of each module",
"read_raw_source": "Open the original text",
"images_collection_not_supported": "Image collection is not supported open the original file",
"reasoning_text": "Thinking process",
"release_cancel": "Release Cancel",
"release_send": "Release send, slide up to cancel",
@ -167,6 +167,8 @@
"start_chat": "Start",
"stream_output": "Stream Output",
"task_has_continued": "Task has continued running",
"tool_input": "tool input",
"tool_output": "Tool output",
"unsupported_file_type": "Unsupported file types",
"upload": "Upload",
"variable_invisable_in_share": "External variables are not visible in login-free links",

View File

@ -427,7 +427,6 @@
"core.chat.response.module query": "Question/Search Term",
"core.chat.response.module similarity": "Similarity",
"core.chat.response.module temperature": "Temperature",
"core.chat.response.plugin output": "Plugin Output Value",
"core.chat.response.search using reRank": "Result Re-Rank",
"core.chat.response.text output": "Text Output",
"core.chat.response.update_var_result": "Variable Update Result (Displays Multiple Variable Update Results in Order)",

View File

@ -14,7 +14,7 @@
"citations": "{{num}}条引用",
"clear_input_value": "清空输入",
"click_contextual_preview": "点击查看上下文预览",
"click_to_add_url": "点击添加链接",
"click_to_add_url": "输入文件链接",
"completion_finish_close": "连接断开",
"completion_finish_content_filter": "触发安全风控",
"completion_finish_function_call": "函数调用",
@ -51,6 +51,7 @@
"home.no_available_tools": "暂无可用工具",
"home.select_tools": "选择工具",
"home.tools": "工具:{{num}}",
"images_collection_not_supported": "图片数据集不支持打开原文",
"in_progress": "进行中",
"input_guide": "输入引导",
"input_guide_lexicon": "词库",
@ -75,7 +76,6 @@
"query_extension_result": "问题优化结果",
"question_tip": "从上到下,为各个模块的响应顺序",
"read_raw_source": "打开原文",
"images_collection_not_supported": "图片数据集不支持打开原文",
"reasoning_text": "思考过程",
"release_cancel": "松开取消",
"release_send": "松开发送,上滑取消",
@ -170,6 +170,8 @@
"start_chat": "开始对话",
"stream_output": "流输出",
"task_has_continued": "任务已继续运行",
"tool_input": "工具输入",
"tool_output": "工具输出",
"unsupported_file_type": "不支持的文件类型",
"upload": "上传",
"variable_invisable_in_share": "外部变量在免登录链接中不可见",

View File

@ -430,7 +430,6 @@
"core.chat.response.module query": "问题/检索词",
"core.chat.response.module similarity": "相似度",
"core.chat.response.module temperature": "温度",
"core.chat.response.plugin output": "插件输出值",
"core.chat.response.search using reRank": "结果重排",
"core.chat.response.text output": "文本输出",
"core.chat.response.update_var_result": "变量更新结果(按顺序展示多个变量更新结果)",

View File

@ -14,7 +14,7 @@
"citations": "{{num}} 筆引用",
"clear_input_value": "清空輸入",
"click_contextual_preview": "點選檢視上下文預覽",
"click_to_add_url": "點擊添加鏈接",
"click_to_add_url": "輸入文件鏈接",
"completion_finish_close": "連接斷開",
"completion_finish_content_filter": "觸發安全風控",
"completion_finish_function_call": "函式呼叫",
@ -51,6 +51,7 @@
"home.no_available_tools": "暫無可用工具",
"home.select_tools": "選擇工具",
"home.tools": "工具:{{num}}",
"images_collection_not_supported": "圖片資料集不支持開啟原文",
"in_progress": "進行中",
"input_guide": "輸入導引",
"input_guide_lexicon": "詞彙庫",
@ -75,7 +76,6 @@
"query_extension_result": "問題優化結果",
"question_tip": "由上至下,各個模組的回應順序",
"read_raw_source": "開啟原文",
"images_collection_not_supported": "圖片資料集不支持開啟原文",
"reasoning_text": "思考過程",
"release_cancel": "鬆開取消",
"release_send": "鬆開傳送,上滑取消",
@ -167,6 +167,8 @@
"start_chat": "開始對話",
"stream_output": "串流輸出",
"task_has_continued": "任務已繼續運行",
"tool_input": "工具輸入",
"tool_output": "工具輸出",
"unsupported_file_type": "不支援的檔案類型",
"upload": "上傳",
"variable_invisable_in_share": "外部變量在免登錄鏈接中不可見",

View File

@ -427,7 +427,6 @@
"core.chat.response.module query": "問題/搜尋詞",
"core.chat.response.module similarity": "相似度",
"core.chat.response.module temperature": "溫度",
"core.chat.response.plugin output": "外掛程式輸出值",
"core.chat.response.search using reRank": "結果重新排名",
"core.chat.response.text output": "文字輸出",
"core.chat.response.update_var_result": "變數更新結果(依序顯示多個變數更新結果)",

View File

@ -30,6 +30,7 @@ import { POST } from '@/web/common/api/request';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { formatFileSize } from '@fastgpt/global/common/file/tools';
import { WorkflowRuntimeContext } from '@/components/core/chat/ChatContainer/context/workflowRuntimeContext';
import { useSafeTranslation } from '@fastgpt/web/hooks/useSafeTranslation';
const FileSelector = ({
value,
@ -53,7 +54,7 @@ const FileSelector = ({
}) => {
const { feConfigs } = useSystemStore();
const { toast } = useToast();
const { t } = useTranslation();
const { t } = useSafeTranslation();
const appId = useContextSelector(WorkflowRuntimeContext, (v) => v.appId);
const chatId = useContextSelector(WorkflowRuntimeContext, (v) => v.chatId);
@ -491,7 +492,7 @@ const FileSelector = ({
</HStack>
{file?.error && (
<Box mt={1} fontSize={'xs'} color={'red.600'}>
{file?.error}
{t(file.error)}
</Box>
)}
</Box>

View File

@ -350,10 +350,8 @@ export const WholeResponseContent = ({
</>
{/* plugin */}
<>
<Row
label={t('common:core.chat.response.plugin output')}
value={activeModule?.pluginOutput}
/>
<Row label={t('chat:tool_input')} value={activeModule?.toolInput} />
<Row label={t('chat:tool_output')} value={activeModule?.pluginOutput} />
</>
{/* text output */}
<Row label={t('common:core.chat.response.text output')} value={activeModule?.textOutput} />

View File

@ -22,7 +22,7 @@ const RenderList: Record<
Component: dynamic(() => import('./templates/Reference'))
},
[FlowNodeInputTypeEnum.fileSelect]: {
Component: dynamic(() => import('./templates/Reference'))
Component: dynamic(() => import('./templates/FileSelect'))
},
[FlowNodeInputTypeEnum.selectApp]: {
Component: dynamic(() => import('./templates/SelectApp'))
@ -135,6 +135,8 @@ const RenderInput = ({ flowInputList, nodeId, CustomComponent, mb = 5 }: Props)
if (!RenderItem) return null;
console.log(renderType, input);
return {
Component: (
<RenderItem.Component inputs={filterProInputs} item={input} nodeId={nodeId} />

View File

@ -0,0 +1,127 @@
import React, { useCallback, useMemo, useState } from 'react';
import type { RenderInputProps } from '../type';
import { Box, Button, HStack, Input, InputGroup, useDisclosure, VStack } from '@chakra-ui/react';
import type { SelectAppItemType } from '@fastgpt/global/core/workflow/template/system/abandoned/runApp/type';
import Avatar from '@fastgpt/web/components/common/Avatar';
import SelectAppModal from '../../../../SelectAppModal';
import { useTranslation } from 'next-i18next';
import { useContextSelector } from 'use-context-selector';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { getAppDetailById } from '@/web/core/app/api';
import { WorkflowActionsContext } from '@/pageComponents/app/detail/WorkflowComponents/context/workflowActionsContext';
import { AppContext } from '@/pageComponents/app/detail/context';
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyDivider from '@fastgpt/web/components/common/MyDivider';
import { getFileIcon } from '@fastgpt/global/common/file/icon';
import MyAvatar from '@fastgpt/web/components/common/Avatar';
import IconButton from '@/pageComponents/account/team/OrgManage/IconButton';
import MyIconButton from '@fastgpt/web/components/common/Icon/button';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
const FileSelectRender = ({ item, nodeId }: RenderInputProps) => {
const { t } = useTranslation();
const onChangeNode = useContextSelector(WorkflowActionsContext, (v) => v.onChangeNode);
const [urlInput, setUrlInput] = useState('');
const values = useMemo(() => {
if (Array.isArray(item.value)) {
return item.value;
}
return [];
}, [item.value]);
const maxSelectFiles = item.maxFiles || 10;
const isMaxSelected = values.length >= maxSelectFiles;
const handleAddUrl = useCallback(
(value: string) => {
if (!value.trim()) return;
onChangeNode({
nodeId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: [value.trim(), ...values]
}
});
setUrlInput('');
},
[item, nodeId, onChangeNode, values]
);
const handleDeleteUrl = useCallback(
(index: number) => {
onChangeNode({
nodeId,
type: 'updateInput',
key: item.key,
value: {
...item,
value: values.filter((_, i) => i !== index)
}
});
},
[item, nodeId, onChangeNode, values]
);
return (
<Box w={'500px'}>
<Box w={'100%'}>
<InputGroup display={'flex'} alignItems={'center'}>
<MyIcon
position={'absolute'}
left={2.5}
name="common/addLight"
w={'1.2rem'}
color={'primary.600'}
zIndex={10}
/>
<Input
isDisabled={isMaxSelected}
value={urlInput}
onChange={(e) => setUrlInput(e.target.value)}
onBlur={(e) => handleAddUrl(e.target.value)}
border={'1.5px dashed'}
borderColor={'myGray.250'}
borderRadius={'md'}
pl={8}
py={1.5}
placeholder={
isMaxSelected ? t('file:reached_max_file_count') : t('chat:click_to_add_url')
}
/>
</InputGroup>
</Box>
{/* Render */}
{values.length > 0 && (
<>
<MyDivider />
<VStack>
{values.map((url, index) => {
const fileIcon = getFileIcon(url, 'common/link');
return (
<Box key={index} w={'full'}>
<HStack py={2} px={3} bg={'white'} borderRadius={'md'} border={'sm'}>
<MyAvatar src={fileIcon} w={'1.2rem'} />
<Box fontSize={'sm'} flex={'1 0 0'} title={url} className="textEllipsis">
{url}
</Box>
{/* Status icon */}
<MyIconButton
icon={'close'}
onClick={() => handleDeleteUrl(index)}
hoverColor="red.600"
hoverBg="red.50"
/>
</HStack>
</Box>
);
})}
</VStack>
</>
)}
</Box>
);
};
export default React.memo(FileSelectRender);